diff --git a/README.md b/README.md index 87b4f44e5..91ac65dab 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ Each component (frontend, admin, backend and database) has its own `.env` file. Each component has a `.env.dist` file. This file contains all environment variables used by the component and can be used as pattern. If you want to use a local `.env`, copy the `.env.dist` and adjust the variables accordingly. -Each component has a `.env.template` file. These files are very important on deploy. +Each component has a `.env.template` file. These files are very important on deploy. They use COMMUNITY_HOST instead of different urls for different modules because in deploy using nginx is expected for routing incoming request to the correct module There is one `.env.dist` in the `deployment/bare_metal/` folder. This `.env.dist` contains all variables used by the components, e.g. unites all `.env.dist` from the components. On deploy, we copy this `.env.dist` to `.env` and set all variables in this new file. The deploy script loads this variables and provides them by the `.env.templates` of each component, creating an `.env` for each component (see in `deployment/bare_metal/start.sh` the `envsubst`). diff --git a/admin/.env.dist b/admin/.env.dist index 66c84dda8..d92f3d9bc 100644 --- a/admin/.env.dist +++ b/admin/.env.dist @@ -1,4 +1,6 @@ -GRAPHQL_URI=http://localhost:4000/graphql -WALLET_AUTH_URL=http://localhost/authenticate?token={token} -WALLET_URL=http://localhost/login +GRAPHQL_URL=http://localhost:4000 +GRAPHQL_PATH=/graphql +WALLET_URL=http://localhost +WALLET_AUTH_PATH=/authenticate?token={token} +WALLET_LOGIN_PATH=/login DEBUG_DISABLE_AUTH=false \ No newline at end of file diff --git a/admin/.env.template b/admin/.env.template index 488c9aba4..11e849271 100644 --- a/admin/.env.template +++ b/admin/.env.template @@ -1,6 +1,8 @@ CONFIG_VERSION=$ADMIN_CONFIG_VERSION -GRAPHQL_URI=$GRAPHQL_URI -WALLET_AUTH_URL=$WALLET_AUTH_URL -WALLET_URL=$WALLET_URL -DEBUG_DISABLE_AUTH=false \ No newline at end of file +COMMUNITY_HOST=$COMMUNITY_HOST +URL_PROTOCOL=$URL_PROTOCOL +WALLET_AUTH_PATH=$WALLET_AUTH_PATH +WALLET_LOGIN_PATH=$WALLET_LOGIN_PATH +GRAPHQL_PATH=$GRAPHQL_PATH +DEBUG_DISABLE_AUTH=false diff --git a/admin/src/components/NavBar.vue b/admin/src/components/NavBar.vue index 2efeda048..4191290f3 100644 --- a/admin/src/components/NavBar.vue +++ b/admin/src/components/NavBar.vue @@ -38,8 +38,8 @@ export default { name: 'navbar', methods: { async logout() { - window.location.assign(CONFIG.WALLET_URL) - // window.location = CONFIG.WALLET_URL + window.location.assign(CONFIG.WALLET_LOGIN_URL) + // window.location = CONFIG.WALLET_LOGIN_URL this.$store.dispatch('logout') await this.$apollo.mutate({ mutation: logout, diff --git a/admin/src/config/index.js b/admin/src/config/index.js index fe373386d..708815398 100644 --- a/admin/src/config/index.js +++ b/admin/src/config/index.js @@ -7,37 +7,45 @@ const pkg = require('../../package') const constants = { CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v1.2022-03-18', + EXPECTED: 'v2.2024-01-04', CURRENT: '', }, } const version = { APP_VERSION: pkg.version, - BUILD_COMMIT: process.env.BUILD_COMMIT || null, + BUILD_COMMIT: process.env.BUILD_COMMIT ?? null, // self reference of `version.BUILD_COMMIT` is not possible at this point, hence the duplicate code - BUILD_COMMIT_SHORT: (process.env.BUILD_COMMIT || '0000000').slice(0, 7), - PORT: process.env.PORT || 8080, + BUILD_COMMIT_SHORT: (process.env.BUILD_COMMIT ?? '0000000').slice(0, 7), + PORT: process.env.PORT ?? 8080, } const environment = { NODE_ENV: process.env.NODE_ENV, - DEBUG: process.env.NODE_ENV !== 'production' || false, - PRODUCTION: process.env.NODE_ENV === 'production' || false, + DEBUG: process.env.NODE_ENV !== 'production' ?? false, + PRODUCTION: process.env.NODE_ENV === 'production' ?? false, } +const COMMUNITY_HOST = process.env.COMMUNITY_HOST ?? undefined +const URL_PROTOCOL = process.env.URL_PROTOCOL ?? 'http' +const COMMUNITY_URL = + COMMUNITY_HOST && URL_PROTOCOL ? URL_PROTOCOL + '://' + COMMUNITY_HOST : undefined +const WALLET_URL = process.env.WALLET_URL ?? COMMUNITY_URL ?? 'http://localhost' + const endpoints = { - GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000/graphql', - WALLET_AUTH_URL: process.env.WALLET_AUTH_URL || 'http://localhost/authenticate?token={token}', - WALLET_URL: process.env.WALLET_URL || 'http://localhost/login', + GRAPHQL_URL: + (process.env.GRAPHQL_URL ?? COMMUNITY_URL ?? 'http://localhost:4000') + + process.env.GRAPHQL_PATH ?? '/graphql', + WALLET_AUTH_URL: WALLET_URL + (process.env.WALLET_AUTH_PATH ?? '/authenticate?token={token}'), + WALLET_LOGIN_URL: WALLET_URL + (process.env.WALLET_LOGIN_PATH ?? '/login'), } const debug = { - DEBUG_DISABLE_AUTH: process.env.DEBUG_DISABLE_AUTH === 'true' || false, + DEBUG_DISABLE_AUTH: process.env.DEBUG_DISABLE_AUTH === 'true' ?? false, } // Check config version -constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT +constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION ?? constants.CONFIG_VERSION.DEFAULT if ( ![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes( constants.CONFIG_VERSION.CURRENT, diff --git a/admin/src/plugins/apolloProvider.js b/admin/src/plugins/apolloProvider.js index 8b02013f4..122857031 100644 --- a/admin/src/plugins/apolloProvider.js +++ b/admin/src/plugins/apolloProvider.js @@ -16,7 +16,7 @@ const authLink = new ApolloLink((operation, forward) => { return forward(operation).map((response) => { if (response.errors && response.errors[0].message === '403.13 - Client certificate revoked') { store.dispatch('logout', null) - window.location.assign(CONFIG.WALLET_URL) + window.location.assign(CONFIG.WALLET_LOGIN_URL) return response } const newToken = operation.getContext().response.headers.get('token') diff --git a/backend/.env.dist b/backend/.env.dist index fb95e984c..4ec60d856 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -27,10 +27,10 @@ DLT_CONNECTOR_URL=http://localhost:6010 # Community COMMUNITY_NAME=Gradido Entwicklung -COMMUNITY_URL=http://localhost/ -COMMUNITY_REGISTER_URL=http://localhost/register -COMMUNITY_REDEEM_URL=http://localhost/redeem/{code} -COMMUNITY_REDEEM_CONTRIBUTION_URL=http://localhost/redeem/CL-{code} +COMMUNITY_URL=http://localhost +COMMUNITY_REGISTER_PATH=/register +COMMUNITY_REDEEM_PATH=/redeem/{code} +COMMUNITY_REDEEM_CONTRIBUTION_PATH=/redeem/CL-{code} COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido. COMMUNITY_SUPPORT_MAIL=support@supportmail.com @@ -47,10 +47,10 @@ EMAIL_SENDER=info@gradido.net EMAIL_PASSWORD=xxx EMAIL_SMTP_URL=gmail.com EMAIL_SMTP_PORT=587 -EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{optin}{code} -EMAIL_LINK_SETPASSWORD=http://localhost/reset-password/{optin} -EMAIL_LINK_FORGOTPASSWORD=http://localhost/forgot-password -EMAIL_LINK_OVERVIEW=http://localhost/overview +EMAIL_LINK_VERIFICATION_PATH=/checkEmail/{optin}{code} +EMAIL_LINK_SETPASSWORD_PATH=/reset-password/{optin} +EMAIL_LINK_FORGOTPASSWORD_PATH=/forgot-password +EMAIL_LINK_OVERVIEW_PATH=/overview EMAIL_CODE_VALID_TIME=1440 EMAIL_CODE_REQUEST_TIME=10 @@ -63,6 +63,7 @@ WEBHOOK_ELOPAGE_SECRET=secret # Federation FEDERATION_VALIDATE_COMMUNITY_TIMER=60000 +FEDERATION_XCOM_SENDCOINS_ENABLED=false # GMS # GMS_ACTIVE=true diff --git a/backend/.env.template b/backend/.env.template index 97fd235ec..00d732ea2 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -1,5 +1,5 @@ # must match the CONFIG_VERSION.EXPECTED definition in scr/config/index.ts -CONFIG_VERSION=v21.2023-11-15 +CONFIG_VERSION=$BACKEND_CONFIG_VERSION # Server JWT_SECRET=$JWT_SECRET @@ -25,14 +25,15 @@ KLICKTIPP_APIKEY_EN=$KLICKTIPP_APIKEY_EN # DltConnector DLT_CONNECTOR=$DLT_CONNECTOR -DLT_CONNECTOR_URL=$DLT_CONNECTOR_URL +DLT_CONNECTOR_PORT=$DLT_CONNECTOR_PORT # Community +COMMUNITY_HOST=$COMMUNITY_HOST +URL_PROTOCOL=$URL_PROTOCOL COMMUNITY_NAME=$COMMUNITY_NAME -COMMUNITY_URL=$COMMUNITY_URL -COMMUNITY_REGISTER_URL=$COMMUNITY_REGISTER_URL -COMMUNITY_REDEEM_URL=$COMMUNITY_REDEEM_URL -COMMUNITY_REDEEM_CONTRIBUTION_URL=$COMMUNITY_REDEEM_CONTRIBUTION_URL +COMMUNITY_REGISTER_PATH=$COMMUNITY_REGISTER_PATH +COMMUNITY_REDEEM_PATH=$COMMUNITY_REDEEM_PATH +COMMUNITY_REDEEM_CONTRIBUTION_PATH=$COMMUNITY_REDEEM_CONTRIBUTION_PATH COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION COMMUNITY_SUPPORT_MAIL=$COMMUNITY_SUPPORT_MAIL @@ -48,12 +49,12 @@ EMAIL_USERNAME=$EMAIL_USERNAME EMAIL_SENDER=$EMAIL_SENDER EMAIL_PASSWORD=$EMAIL_PASSWORD EMAIL_SMTP_URL=$EMAIL_SMTP_URL -EMAIL_SMTP_PORT=587 -EMAIL_LINK_VERIFICATION=$EMAIL_LINK_VERIFICATION -EMAIL_LINK_SETPASSWORD=$EMAIL_LINK_SETPASSWORD -EMAIL_LINK_FORGOTPASSWORD=$EMAIL_LINK_FORGOTPASSWORD -EMAIL_LINK_OVERVIEW=$EMAIL_LINK_OVERVIEW -EMAIL_CODE_VALID_TIME=$EMAIL_CODE_VALID_TIME +EMAIL_SMTP_PORT=$EMAIL_SMTP_PORT +EMAIL_LINK_VERIFICATION_PATH=$EMAIL_LINK_VERIFICATION_PATH +EMAIL_LINK_SETPASSWORD_PATH=$EMAIL_LINK_SETPASSWORD_PATH +EMAIL_LINK_FORGOTPASSWORD_PATH=$EMAIL_LINK_FORGOTPASSWORD_PATH +EMAIL_LINK_OVERVIEW_PATH=$EMAIL_LINK_OVERVIEW_PATH +EMAIL_CODE_VALID_TIME=$EMAIL_CODE_VALID_TIME_PATH EMAIL_CODE_REQUEST_TIME=$EMAIL_CODE_REQUEST_TIME # Webhook @@ -61,6 +62,7 @@ WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET # Federation FEDERATION_VALIDATE_COMMUNITY_TIMER=$FEDERATION_VALIDATE_COMMUNITY_TIMER +FEDERATION_XCOM_SENDCOINS_ENABLED=$FEDERATION_XCOM_SENDCOINS_ENABLED # GMS GMS_ACTIVE=$GMS_ACTIVE diff --git a/backend/jest.config.js b/backend/jest.config.js index 625dca00f..f7edec3dd 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', + '@dltConnector/(.*)': '/src/apis/dltConnector/$1', '@enum/(.*)': '/src/graphql/enum/$1', '@model/(.*)': '/src/graphql/model/$1', '@union/(.*)': '/src/graphql/union/$1', @@ -27,6 +28,11 @@ module.exports = { process.env.NODE_ENV === 'development' ? '/../database/entity/$1' : '/../database/build/entity/$1', + '@logging/(.*)': + // eslint-disable-next-line n/no-process-env + process.env.NODE_ENV === 'development' + ? '/../database/logging/$1' + : '/../database/build/logging/$1', '@dbTools/(.*)': // eslint-disable-next-line n/no-process-env process.env.NODE_ENV === 'development' diff --git a/backend/package.json b/backend/package.json index a33b9bbd7..81cdb5111 100644 --- a/backend/package.json +++ b/backend/package.json @@ -31,9 +31,11 @@ "dotenv": "^10.0.0", "email-templates": "^10.0.1", "express": "^4.17.1", + "express-slow-down": "^2.0.1", "gradido-database": "file:../database", "graphql": "^15.5.1", "graphql-request": "5.0.0", + "helmet": "^5.1.1", "i18n": "^0.15.1", "jose": "^4.14.4", "lodash.clonedeep": "^4.5.0", diff --git a/backend/src/apis/DltConnectorClient.test.ts b/backend/src/apis/dltConnector/DltConnectorClient.test.ts similarity index 84% rename from backend/src/apis/DltConnectorClient.test.ts rename to backend/src/apis/dltConnector/DltConnectorClient.test.ts index 56fa3d13f..d99093a1b 100644 --- a/backend/src/apis/DltConnectorClient.test.ts +++ b/backend/src/apis/dltConnector/DltConnectorClient.test.ts @@ -25,8 +25,6 @@ let testEnv: { jest.mock('graphql-request', () => { const originalModule = jest.requireActual('graphql-request') - let testCursor = 0 - return { __esModule: true, ...originalModule, @@ -38,30 +36,11 @@ jest.mock('graphql-request', () => { // why not using mockResolvedValueOnce or mockReturnValueOnce? // I have tried, but it didn't work and return every time the first value request: jest.fn().mockImplementation(() => { - testCursor++ - if (testCursor === 4) { - return Promise.resolve( - // invalid, is 33 Bytes long as binary - { - transmitTransaction: { - dltTransactionIdHex: - '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516212A', - }, - }, - ) - } else if (testCursor === 5) { - throw Error('Connection error') - } else { - return Promise.resolve( - // valid, is 32 Bytes long as binary - { - transmitTransaction: { - dltTransactionIdHex: - '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621', - }, - }, - ) - } + return Promise.resolve({ + transmitTransaction: { + succeed: true, + }, + }) }), } }), diff --git a/backend/src/apis/DltConnectorClient.ts b/backend/src/apis/dltConnector/DltConnectorClient.ts similarity index 67% rename from backend/src/apis/DltConnectorClient.ts rename to backend/src/apis/dltConnector/DltConnectorClient.ts index f01a55d6c..765d09fb4 100644 --- a/backend/src/apis/DltConnectorClient.ts +++ b/backend/src/apis/dltConnector/DltConnectorClient.ts @@ -6,6 +6,9 @@ import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId' import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' +import { TransactionResult } from './model/TransactionResult' +import { UserIdentifier } from './model/UserIdentifier' + const sendTransaction = gql` mutation ($input: TransactionInput!) { sendTransaction(data: $input) { @@ -78,32 +81,42 @@ export class DltConnectorClient { * transmit transaction via dlt-connector to iota * and update dltTransactionId of transaction in db with iota message id */ - public async transmitTransaction( - transaction: DbTransaction, - senderCommunityUuid?: string, - recipientCommunityUuid?: string, - ): Promise { + public async transmitTransaction(transaction: DbTransaction): Promise { const typeString = getTransactionTypeString(transaction.typeId) - const amountString = transaction.amount.toString() + // no negative values in dlt connector, gradido concept don't use negative values so the code don't use it too + const amountString = transaction.amount.abs().toString() + const params = { + input: { + user: { + uuid: transaction.userGradidoID, + communityUuid: transaction.userCommunityUuid, + } as UserIdentifier, + linkedUser: { + uuid: transaction.linkedUserGradidoID, + communityUuid: transaction.linkedUserCommunityUuid, + } as UserIdentifier, + amount: amountString, + type: typeString, + createdAt: transaction.balanceDate.toISOString(), + backendTransactionId: transaction.id, + targetDate: transaction.creationDate?.toISOString(), + }, + } try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { data } = await this.client.rawRequest(sendTransaction, { - input: { - senderUser: { - uuid: transaction.userGradidoID, - communityUuid: senderCommunityUuid, - }, - recipientUser: { - uuid: transaction.linkedUserGradidoID, - communityUuid: recipientCommunityUuid, - }, - amount: amountString, - type: typeString, - createdAt: transaction.balanceDate.toString(), + // TODO: add account nr for user after they have also more than one account in backend + logger.debug('transmit transaction to dlt connector', params) + const { + data: { + sendTransaction: { error, succeed }, }, - }) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access - return data.sendTransaction.dltTransactionIdHex + } = await this.client.rawRequest<{ sendTransaction: TransactionResult }>( + sendTransaction, + params, + ) + if (error) { + throw new Error(error.message) + } + return succeed } catch (e) { throw new LogError('Error send sending transaction to dlt-connector: ', e) } diff --git a/backend/src/apis/dltConnector/enum/TransactionErrorType.ts b/backend/src/apis/dltConnector/enum/TransactionErrorType.ts new file mode 100644 index 000000000..5a2c5485e --- /dev/null +++ b/backend/src/apis/dltConnector/enum/TransactionErrorType.ts @@ -0,0 +1,14 @@ +/** + * Error Types for dlt-connector graphql responses + */ +export enum TransactionErrorType { + NOT_IMPLEMENTED_YET = 'Not Implemented yet', + MISSING_PARAMETER = 'Missing parameter', + ALREADY_EXIST = 'Already exist', + DB_ERROR = 'DB Error', + PROTO_DECODE_ERROR = 'Proto Decode Error', + PROTO_ENCODE_ERROR = 'Proto Encode Error', + INVALID_SIGNATURE = 'Invalid Signature', + LOGIC_ERROR = 'Logic Error', + NOT_FOUND = 'Not found', +} diff --git a/backend/src/apis/dltConnector/enum/TransactionType.ts b/backend/src/apis/dltConnector/enum/TransactionType.ts new file mode 100644 index 000000000..51b87c134 --- /dev/null +++ b/backend/src/apis/dltConnector/enum/TransactionType.ts @@ -0,0 +1,11 @@ +/** + * Transaction Types on Blockchain + */ +export enum TransactionType { + GRADIDO_TRANSFER = 1, + GRADIDO_CREATION = 2, + GROUP_FRIENDS_UPDATE = 3, + REGISTER_ADDRESS = 4, + GRADIDO_DEFERRED_TRANSFER = 5, + COMMUNITY_ROOT = 6, +} diff --git a/backend/src/apis/dltConnector/model/TransactionError.ts b/backend/src/apis/dltConnector/model/TransactionError.ts new file mode 100644 index 000000000..a2b1348a5 --- /dev/null +++ b/backend/src/apis/dltConnector/model/TransactionError.ts @@ -0,0 +1,7 @@ +import { TransactionErrorType } from '@dltConnector/enum/TransactionErrorType' + +export interface TransactionError { + type: TransactionErrorType + message: string + name: string +} diff --git a/backend/src/apis/dltConnector/model/TransactionRecipe.ts b/backend/src/apis/dltConnector/model/TransactionRecipe.ts new file mode 100644 index 000000000..edd7deadb --- /dev/null +++ b/backend/src/apis/dltConnector/model/TransactionRecipe.ts @@ -0,0 +1,8 @@ +import { TransactionType } from '@dltConnector/enum/TransactionType' + +export interface TransactionRecipe { + id: number + createdAt: string + type: TransactionType + topic: string +} diff --git a/backend/src/apis/dltConnector/model/TransactionResult.ts b/backend/src/apis/dltConnector/model/TransactionResult.ts new file mode 100644 index 000000000..510907429 --- /dev/null +++ b/backend/src/apis/dltConnector/model/TransactionResult.ts @@ -0,0 +1,8 @@ +import { TransactionError } from './TransactionError' +import { TransactionRecipe } from './TransactionRecipe' + +export interface TransactionResult { + error?: TransactionError + recipe?: TransactionRecipe + succeed: boolean +} diff --git a/backend/src/apis/dltConnector/model/UserIdentifier.ts b/backend/src/apis/dltConnector/model/UserIdentifier.ts new file mode 100644 index 000000000..e265593be --- /dev/null +++ b/backend/src/apis/dltConnector/model/UserIdentifier.ts @@ -0,0 +1,5 @@ +export interface UserIdentifier { + uuid: string + communityUuid: string + accountNr?: number +} diff --git a/backend/src/apis/gms/model/GmsEnums.ts b/backend/src/apis/gms/model/GmsEnums.ts deleted file mode 100644 index 1342327d1..000000000 --- a/backend/src/apis/gms/model/GmsEnums.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { registerEnumType } from 'type-graphql' - -export enum GmsPublishNameType { - GMS_PUBLISH_NAME_ALIAS_OR_INITALS = 0, - GMS_PUBLISH_NAME_INITIALS = 1, - GMS_PUBLISH_NAME_FIRST = 2, - GMS_PUBLISH_NAME_FIRST_INITIAL = 3, - GMS_PUBLISH_NAME_FULL = 4, -} - -registerEnumType(GmsPublishNameType, { - name: 'GmsPublishNameType', // this one is mandatory - description: 'Type of name publishing', // this one is optional -}) - -export enum GmsPublishPhoneType { - GMS_PUBLISH_PHONE_NOTHING = 0, - GMS_PUBLISH_PHONE_COUNTRY = 1, - GMS_PUBLISH_PHONE_FULL = 2, -} - -registerEnumType(GmsPublishPhoneType, { - name: 'GmsPublishPhoneType', // this one is mandatory - description: 'Type of Phone publishing', // this one is optional -}) - -export enum GmsLocationType { - GMS_LOCATION_TYPE_EXACT = 0, - GMS_LOCATION_TYPE_APPROXIMATE = 1, - GMS_LOCATION_TYPE_RANDOM = 2, -} - -registerEnumType(GmsLocationType, { - name: 'GmsLocationType', // this one is mandatory - description: 'Type of location treatment in GMS', // this one is optional -}) diff --git a/backend/src/apis/gms/model/GmsUser.ts b/backend/src/apis/gms/model/GmsUser.ts index 2fad3bd1e..7f7db7660 100644 --- a/backend/src/apis/gms/model/GmsUser.ts +++ b/backend/src/apis/gms/model/GmsUser.ts @@ -1,6 +1,8 @@ import { User as dbUser } from '@entity/User' -import { GmsLocationType, GmsPublishNameType, GmsPublishPhoneType } from './GmsEnums' +import { GmsPublishLocationType } from '@/graphql/enum/GmsPublishLocationType' +import { GmsPublishNameType } from '@/graphql/enum/GmsPublishNameType' +import { GmsPublishPhoneType } from '@/graphql/enum/GmsPublishPhoneType' export class GmsUser { constructor(user: dbUser) { @@ -12,7 +14,7 @@ export class GmsUser { this.firstName = this.getGmsFirstName(user) this.lastName = this.getGmsLastName(user) this.alias = this.getGmsAlias(user) - this.type = GmsLocationType.GMS_LOCATION_TYPE_RANDOM + this.type = GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM this.location = null } diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 1c88fcd8e..b94178157 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -12,14 +12,14 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0079-introduce_gms_registration', + DB_VERSION: '0082-introduce_gms_registration', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info LOG_LEVEL: process.env.LOG_LEVEL ?? 'info', CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v21.2023-11-15', + EXPECTED: 'v21.2024-01-06', CURRENT: '', }, } @@ -51,18 +51,23 @@ const klicktipp = { KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN ?? 'SomeFakeKeyEN', } +const COMMUNITY_HOST = process.env.COMMUNITY_HOST ?? 'localhost' +const URL_PROTOCOL = process.env.URL_PROTOCOL ?? 'http' +const COMMUNITY_URL = process.env.COMMUNITY_URL ?? `${URL_PROTOCOL}://${COMMUNITY_HOST}` +const DLT_CONNECTOR_PORT = process.env.DLT_CONNECTOR_PORT ?? 6010 + const dltConnector = { DLT_CONNECTOR: process.env.DLT_CONNECTOR === 'true' || false, - DLT_CONNECTOR_URL: process.env.DLT_CONNECTOR_URL ?? 'http://localhost:6010', + DLT_CONNECTOR_URL: process.env.DLT_CONNECTOR_URL ?? `${COMMUNITY_URL}:${DLT_CONNECTOR_PORT}`, } const community = { COMMUNITY_NAME: process.env.COMMUNITY_NAME ?? 'Gradido Entwicklung', - COMMUNITY_URL: process.env.COMMUNITY_URL ?? 'http://localhost/', - COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL ?? 'http://localhost/register', - COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL ?? 'http://localhost/redeem/{code}', + COMMUNITY_URL, + COMMUNITY_REGISTER_URL: COMMUNITY_URL + (process.env.COMMUNITY_REGISTER_PATH ?? '/register'), + COMMUNITY_REDEEM_URL: COMMUNITY_URL + (process.env.COMMUNITY_REDEEM_PATH ?? '/redeem/{code}'), COMMUNITY_REDEEM_CONTRIBUTION_URL: - process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL ?? 'http://localhost/redeem/CL-{code}', + COMMUNITY_URL + (process.env.COMMUNITY_REDEEM_CONTRIBUTION_PATH ?? '/redeem/CL-{code}'), COMMUNITY_DESCRIPTION: process.env.COMMUNITY_DESCRIPTION ?? 'Die lokale Entwicklungsumgebung von Gradido.', COMMUNITY_SUPPORT_MAIL: process.env.COMMUNITY_SUPPORT_MAIL ?? 'support@supportmail.com', @@ -74,8 +79,8 @@ const loginServer = { } const email = { - EMAIL: process.env.EMAIL === 'true' || false, - EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' || false, + EMAIL: process.env.EMAIL === 'true' ?? false, + EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' ?? false, EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER ?? 'stage1@gradido.net', EMAIL_USERNAME: process.env.EMAIL_USERNAME ?? '', EMAIL_SENDER: process.env.EMAIL_SENDER ?? 'info@gradido.net', @@ -85,19 +90,19 @@ const email = { // eslint-disable-next-line no-unneeded-ternary EMAIL_TLS: process.env.EMAIL_TLS === 'false' ? false : true, EMAIL_LINK_VERIFICATION: - process.env.EMAIL_LINK_VERIFICATION ?? 'http://localhost/checkEmail/{optin}{code}', + COMMUNITY_URL + (process.env.EMAIL_LINK_VERIFICATION_PATH ?? '/checkEmail/{optin}{code}'), EMAIL_LINK_SETPASSWORD: - process.env.EMAIL_LINK_SETPASSWORD ?? 'http://localhost/reset-password/{optin}', + COMMUNITY_URL + (process.env.EMAIL_LINK_SETPASSWORD_PATH ?? '/reset-password/{optin}'), EMAIL_LINK_FORGOTPASSWORD: - process.env.EMAIL_LINK_FORGOTPASSWORD ?? 'http://localhost/forgot-password', - EMAIL_LINK_OVERVIEW: process.env.EMAIL_LINK_OVERVIEW ?? 'http://localhost/overview', + COMMUNITY_URL + (process.env.EMAIL_LINK_FORGOTPASSWORD_PATH ?? '/forgot-password'), + EMAIL_LINK_OVERVIEW: COMMUNITY_URL + (process.env.EMAIL_LINK_OVERVIEW_PATH ?? '/overview'), // time in minutes a optin code is valid EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME - ? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 1440 + ? parseInt(process.env.EMAIL_CODE_VALID_TIME) ?? 1440 : 1440, // time in minutes that must pass to request a new optin code EMAIL_CODE_REQUEST_TIME: process.env.EMAIL_CODE_REQUEST_TIME - ? parseInt(process.env.EMAIL_CODE_REQUEST_TIME) || 10 + ? parseInt(process.env.EMAIL_CODE_REQUEST_TIME) ?? 10 : 10, } @@ -124,9 +129,9 @@ if ( const federation = { FEDERATION_BACKEND_SEND_ON_API: process.env.FEDERATION_BACKEND_SEND_ON_API ?? '1_0', FEDERATION_VALIDATE_COMMUNITY_TIMER: - Number(process.env.FEDERATION_VALIDATE_COMMUNITY_TIMER) || 60000, + Number(process.env.FEDERATION_VALIDATE_COMMUNITY_TIMER) ?? 60000, FEDERATION_XCOM_SENDCOINS_ENABLED: - process.env.FEDERATION_XCOM_SENDCOINS_ENABLED === 'true' || false, + process.env.FEDERATION_XCOM_SENDCOINS_ENABLED === 'true' ?? false, // default value for community-uuid is equal uuid of stage-3 FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID: process.env.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID ?? '56a55482-909e-46a4-bfa2-cd025e894ebc', diff --git a/backend/src/emails/templates/addedContributionMessage/html.pug b/backend/src/emails/templates/addedContributionMessage/html.pug index 7865b2099..ff7c89c30 100644 --- a/backend/src/emails/templates/addedContributionMessage/html.pug +++ b/backend/src/emails/templates/addedContributionMessage/html.pug @@ -9,6 +9,6 @@ block content h2= t('emails.addedContributionMessage.readMessage') div(class="p_content")= t('emails.addedContributionMessage.toSeeAndAnswerMessage') - a.button-3(href=`${communityURL}community/contributions`) #{t('emails.general.toAccount')} + a.button-3(href=`${communityURL}/community/contributions`) #{t('emails.general.toAccount')} include ../includes/doNotReply.pug diff --git a/backend/src/emails/templates/includes/contributionDetailsCTA.pug b/backend/src/emails/templates/includes/contributionDetailsCTA.pug index fb2906419..0a3bd395d 100644 --- a/backend/src/emails/templates/includes/contributionDetailsCTA.pug +++ b/backend/src/emails/templates/includes/contributionDetailsCTA.pug @@ -1,7 +1,7 @@ //- h2= t('emails.general.contributionDetails') div(class="p_content")= t('emails.contribution.toSeeContributionsAndMessages') -a.button-3(href=`${communityURL}community/contributions`) #{t('emails.general.toAccount')} +a.button-3(href=`${communityURL}/community/contributions`) #{t('emails.general.toAccount')} div(class="p_content")= t('emails.general.orCopyLink') -a.clink(href=`${communityURL}community/contributions`) #{`${communityURL}community/contributions`} \ No newline at end of file +a.clink(href=`${communityURL}/community/contributions`) #{`${communityURL}/community/contributions`} \ No newline at end of file diff --git a/backend/src/emails/templates/transactionLinkRedeemed/html.pug b/backend/src/emails/templates/transactionLinkRedeemed/html.pug index b24c5da40..281ee9205 100644 --- a/backend/src/emails/templates/transactionLinkRedeemed/html.pug +++ b/backend/src/emails/templates/transactionLinkRedeemed/html.pug @@ -13,6 +13,6 @@ block content br = t('emails.general.detailsYouFindOnLinkToYourAccount') - a.button-3(href=`${communityURL}transactions`) #{t('emails.general.toAccount')} + a.button-3(href=`${communityURL}/transactions`) #{t('emails.general.toAccount')} include ../includes/doNotReply.pug diff --git a/backend/src/emails/templates/transactionReceived/html.pug b/backend/src/emails/templates/transactionReceived/html.pug index 93de2c88e..5370ec03e 100644 --- a/backend/src/emails/templates/transactionReceived/html.pug +++ b/backend/src/emails/templates/transactionReceived/html.pug @@ -9,7 +9,7 @@ block content h2= t('emails.general.transactionDetails') div(class="p_content")= t('emails.general.detailsYouFindOnLinkToYourAccount') - a.button-3(href=`${communityURL}transactions`) #{t('emails.general.toAccount')} + a.button-3(href=`${communityURL}/transactions`) #{t('emails.general.toAccount')} include ../includes/doNotReply.pug diff --git a/backend/src/federation/client/1_0/AuthenticationClient.ts b/backend/src/federation/client/1_0/AuthenticationClient.ts index abc903778..f73393255 100644 --- a/backend/src/federation/client/1_0/AuthenticationClient.ts +++ b/backend/src/federation/client/1_0/AuthenticationClient.ts @@ -28,9 +28,9 @@ export class AuthenticationClient { async openConnection(args: OpenConnectionArgs): Promise { logger.debug(`Authentication: openConnection at ${this.endpoint} for args:`, args) try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { data } = await this.client.rawRequest(openConnection, { args }) - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const { data } = await this.client.rawRequest<{ openConnection: boolean }>(openConnection, { + args, + }) if (!data?.openConnection) { logger.warn( 'Authentication: openConnection without response data from endpoint', diff --git a/backend/src/federation/client/1_0/FederationClient.ts b/backend/src/federation/client/1_0/FederationClient.ts index 8d915c751..b9939a12c 100644 --- a/backend/src/federation/client/1_0/FederationClient.ts +++ b/backend/src/federation/client/1_0/FederationClient.ts @@ -5,9 +5,10 @@ import { getPublicCommunityInfo } from '@/federation/client/1_0/query/getPublicC import { getPublicKey } from '@/federation/client/1_0/query/getPublicKey' import { backendLogger as logger } from '@/server/logger' +import { PublicCommunityInfoLoggingView } from './logging/PublicCommunityInfoLogging.view' +import { GetPublicKeyResult } from './model/GetPublicKeyResult' import { PublicCommunityInfo } from './model/PublicCommunityInfo' -// eslint-disable-next-line camelcase export class FederationClient { dbCom: DbFederatedCommunity endpoint: string @@ -27,12 +28,17 @@ export class FederationClient { }) } + getEndpoint = () => { + return this.endpoint + } + getPublicKey = async (): Promise => { logger.debug('Federation: getPublicKey from endpoint', this.endpoint) try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { data } = await this.client.rawRequest(getPublicKey, {}) - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const { data } = await this.client.rawRequest<{ getPublicKey: GetPublicKeyResult }>( + getPublicKey, + {}, + ) if (!data?.getPublicKey?.publicKey) { logger.warn('Federation: getPublicKey without response data from endpoint', this.endpoint) return @@ -40,22 +46,25 @@ export class FederationClient { logger.debug( 'Federation: getPublicKey successful from endpoint', this.endpoint, - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access data.getPublicKey.publicKey, ) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access return data.getPublicKey.publicKey } catch (err) { - logger.warn('Federation: getPublicKey failed for endpoint', this.endpoint) + const errorString = JSON.stringify(err) + logger.warn('Federation: getPublicKey failed for endpoint', { + endpoint: this.endpoint, + err: errorString.length <= 200 ? errorString : errorString.substring(0, 200) + '...', + }) } } getPublicCommunityInfo = async (): Promise => { logger.debug(`Federation: getPublicCommunityInfo with endpoint='${this.endpoint}'...`) try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { data } = await this.client.rawRequest(getPublicCommunityInfo, {}) - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const { data } = await this.client.rawRequest<{ + getPublicCommunityInfo: PublicCommunityInfo + }>(getPublicCommunityInfo, {}) + if (!data?.getPublicCommunityInfo?.name) { logger.warn( 'Federation: getPublicCommunityInfo without response data from endpoint', @@ -64,12 +73,17 @@ export class FederationClient { return } logger.debug(`Federation: getPublicCommunityInfo successful from endpoint=${this.endpoint}`) - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - logger.debug(`publicCommunityInfo:`, data.getPublicCommunityInfo) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access + logger.debug( + `publicCommunityInfo:`, + new PublicCommunityInfoLoggingView(data.getPublicCommunityInfo), + ) return data.getPublicCommunityInfo } catch (err) { - logger.warn('Federation: getPublicCommunityInfo failed for endpoint', this.endpoint) + const errorString = JSON.stringify(err) + logger.warn('Federation: getPublicCommunityInfo failed for endpoint', { + endpoint: this.endpoint, + err: errorString.length <= 200 ? errorString : errorString.substring(0, 200) + '...', + }) } } } diff --git a/backend/src/federation/client/1_0/SendCoinsClient.ts b/backend/src/federation/client/1_0/SendCoinsClient.ts index c96961103..bcf303584 100644 --- a/backend/src/federation/client/1_0/SendCoinsClient.ts +++ b/backend/src/federation/client/1_0/SendCoinsClient.ts @@ -4,6 +4,8 @@ import { GraphQLClient } from 'graphql-request' import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' +import { SendCoinsArgsLoggingView } from './logging/SendCoinsArgsLogging.view' +import { SendCoinsResultLoggingView } from './logging/SendCoinsResultLogging.view' import { SendCoinsArgs } from './model/SendCoinsArgs' import { SendCoinsResult } from './model/SendCoinsResult' import { revertSendCoins as revertSendCoinsQuery } from './query/revertSendCoins' @@ -11,7 +13,6 @@ import { revertSettledSendCoins as revertSettledSendCoinsQuery } from './query/r import { settleSendCoins as settleSendCoinsQuery } from './query/settleSendCoins' import { voteForSendCoins as voteForSendCoinsQuery } from './query/voteForSendCoins' -// eslint-disable-next-line camelcase export class SendCoinsClient { dbCom: DbFederatedCommunity endpoint: string @@ -34,26 +35,26 @@ export class SendCoinsClient { async voteForSendCoins(args: SendCoinsArgs): Promise { logger.debug('X-Com: voteForSendCoins against endpoint=', this.endpoint) try { - logger.debug(`X-Com: SendCoinsClient: voteForSendCoins with args=`, args) - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { data } = await this.client.rawRequest(voteForSendCoinsQuery, { args }) - logger.debug(`X-Com: SendCoinsClient: after rawRequest...data:`, data) - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + logger.debug( + `X-Com: SendCoinsClient: voteForSendCoins with args=`, + new SendCoinsArgsLoggingView(args), + ) + const { data } = await this.client.rawRequest<{ voteForSendCoins: SendCoinsResult }>( + voteForSendCoinsQuery, + { args }, + ) + const result = data.voteForSendCoins if (!data?.voteForSendCoins?.vote) { - logger.debug('X-Com: voteForSendCoins failed with: ', data) + logger.debug( + 'X-Com: voteForSendCoins failed with: ', + new SendCoinsResultLoggingView(result), + ) return new SendCoinsResult() } - const result = new SendCoinsResult() - result.vote = true - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment - result.recipGradidoID = data.voteForSendCoins.recipGradidoID - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment - result.recipFirstName = data.voteForSendCoins.recipFirstName - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment - result.recipLastName = data.voteForSendCoins.recipLastName - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment - result.recipAlias = data.voteForSendCoins.recipAlias - logger.debug('X-Com: voteForSendCoins successful with result=', result) + logger.debug( + 'X-Com: voteForSendCoins successful with result=', + new SendCoinsResultLoggingView(result), + ) return result } catch (err) { throw new LogError(`X-Com: voteForSendCoins failed for endpoint=${this.endpoint}:`, err) @@ -63,11 +64,15 @@ export class SendCoinsClient { async revertSendCoins(args: SendCoinsArgs): Promise { logger.debug('X-Com: revertSendCoins against endpoint=', this.endpoint) try { - logger.debug(`X-Com: SendCoinsClient: revertSendCoins with args=`, args) - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { data } = await this.client.rawRequest(revertSendCoinsQuery, { args }) + logger.debug( + `X-Com: SendCoinsClient: revertSendCoins with args=`, + new SendCoinsArgsLoggingView(args), + ) + const { data } = await this.client.rawRequest<{ revertSendCoins: boolean }>( + revertSendCoinsQuery, + { args }, + ) logger.debug(`X-Com: SendCoinsClient: after revertSendCoins: data=`, data) - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (!data?.revertSendCoins) { logger.warn('X-Com: revertSendCoins without response data from endpoint', this.endpoint) return false @@ -88,11 +93,15 @@ export class SendCoinsClient { async settleSendCoins(args: SendCoinsArgs): Promise { logger.debug(`X-Com: settleSendCoins against endpoint='${this.endpoint}'...`) try { - logger.debug(`X-Com: SendCoinsClient: settleSendCoins with args=`, args) - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { data } = await this.client.rawRequest(settleSendCoinsQuery, { args }) + logger.debug( + `X-Com: SendCoinsClient: settleSendCoins with args=`, + new SendCoinsArgsLoggingView(args), + ) + const { data } = await this.client.rawRequest<{ settleSendCoins: boolean }>( + settleSendCoinsQuery, + { args }, + ) logger.debug(`X-Com: SendCoinsClient: after settleSendCoins: data=`, data) - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (!data?.settleSendCoins) { logger.warn( 'X-Com: SendCoinsClient: settleSendCoins without response data from endpoint', @@ -115,9 +124,14 @@ export class SendCoinsClient { async revertSettledSendCoins(args: SendCoinsArgs): Promise { logger.debug(`X-Com: revertSettledSendCoins against endpoint='${this.endpoint}'...`) try { - logger.debug(`X-Com: SendCoinsClient: revertSettledSendCoins with args=`, args) - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { data } = await this.client.rawRequest(revertSettledSendCoinsQuery, { args }) + logger.debug( + `X-Com: SendCoinsClient: revertSettledSendCoins with args=`, + new SendCoinsArgsLoggingView(args), + ) + const { data } = await this.client.rawRequest<{ revertSettledSendCoins: boolean }>( + revertSettledSendCoinsQuery, + { args }, + ) logger.debug(`X-Com: SendCoinsClient: after revertSettledSendCoins: data=`, data) // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (!data?.revertSettledSendCoins) { diff --git a/backend/src/federation/client/1_0/logging/PublicCommunityInfoLogging.view.ts b/backend/src/federation/client/1_0/logging/PublicCommunityInfoLogging.view.ts new file mode 100644 index 000000000..3151bbb31 --- /dev/null +++ b/backend/src/federation/client/1_0/logging/PublicCommunityInfoLogging.view.ts @@ -0,0 +1,19 @@ +import { AbstractLoggingView } from '@logging/AbstractLogging.view' + +import { PublicCommunityInfo } from '@/federation/client/1_0/model/PublicCommunityInfo' + +export class PublicCommunityInfoLoggingView extends AbstractLoggingView { + public constructor(private self: PublicCommunityInfo) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + name: this.self.name, + description: this.self.description, + creationDate: this.dateToString(this.self.creationDate), + publicKey: this.self.publicKey, + } + } +} diff --git a/backend/src/federation/client/1_0/logging/SendCoinsArgsLogging.view.ts b/backend/src/federation/client/1_0/logging/SendCoinsArgsLogging.view.ts new file mode 100644 index 000000000..2df149133 --- /dev/null +++ b/backend/src/federation/client/1_0/logging/SendCoinsArgsLogging.view.ts @@ -0,0 +1,24 @@ +import { AbstractLoggingView } from '@logging/AbstractLogging.view' + +import { SendCoinsArgs } from '@/federation/client/1_0/model/SendCoinsArgs' + +export class SendCoinsArgsLoggingView extends AbstractLoggingView { + public constructor(private self: SendCoinsArgs) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + recipientCommunityUuid: this.self.recipientCommunityUuid, + recipientUserIdentifier: this.self.recipientUserIdentifier, + creationDate: this.self.creationDate, + amount: this.decimalToString(this.self.amount), + memoLength: this.self.memo.length, + senderCommunityUuid: this.self.senderCommunityUuid, + senderUserUuid: this.self.senderUserUuid, + senderUserName: this.self.senderUserName.substring(0, 3), + senderAlias: this.self.senderAlias?.substring(0, 3), + } + } +} diff --git a/backend/src/federation/client/1_0/logging/SendCoinsResultLogging.view.ts b/backend/src/federation/client/1_0/logging/SendCoinsResultLogging.view.ts new file mode 100644 index 000000000..b605eb1db --- /dev/null +++ b/backend/src/federation/client/1_0/logging/SendCoinsResultLogging.view.ts @@ -0,0 +1,20 @@ +import { AbstractLoggingView } from '@logging/AbstractLogging.view' + +import { SendCoinsResult } from '@/federation/client/1_0/model/SendCoinsResult' + +export class SendCoinsResultLoggingView extends AbstractLoggingView { + public constructor(private self: SendCoinsResult) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + vote: this.self.vote, + recipGradidoID: this.self.recipGradidoID, + recipFirstName: this.self.recipFirstName?.substring(0, 3), + recipLastName: this.self.recipLastName?.substring(0, 3), + recipAlias: this.self.recipAlias?.substring(0, 3), + } + } +} diff --git a/backend/src/federation/client/1_0/model/GetPublicKeyResult.ts b/backend/src/federation/client/1_0/model/GetPublicKeyResult.ts new file mode 100644 index 000000000..696c96cfe --- /dev/null +++ b/backend/src/federation/client/1_0/model/GetPublicKeyResult.ts @@ -0,0 +1,13 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { Field, ObjectType } from 'type-graphql' + +@ObjectType() +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export class GetPublicKeyResult { + constructor(pubKey: string) { + this.publicKey = pubKey + } + + @Field(() => String) + publicKey: string +} diff --git a/backend/src/federation/client/FederationClientFactory.ts b/backend/src/federation/client/FederationClientFactory.ts index d057ffd04..fe2ff0dbd 100644 --- a/backend/src/federation/client/FederationClientFactory.ts +++ b/backend/src/federation/client/FederationClientFactory.ts @@ -47,15 +47,25 @@ export class FederationClientFactory { const instance = FederationClientFactory.instanceArray.find( (instance) => instance.id === dbCom.id, ) - if (instance) { + // TODO: found a way to prevent double code with FederationClient::constructor + const endpoint = `${dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'}${ + dbCom.apiVersion + }/` + // check if endpoint is still the same and not changed meanwhile + if (instance && instance.client.getEndpoint() === endpoint) { return instance.client } const client = FederationClientFactory.createFederationClient(dbCom) if (client) { - FederationClientFactory.instanceArray.push({ - id: dbCom.id, - client, - } as FederationClientInstance) + // only update instance if we already have one + if (instance) { + instance.client = client + } else { + FederationClientFactory.instanceArray.push({ + id: dbCom.id, + client, + } as FederationClientInstance) + } } return client } diff --git a/backend/src/federation/validateCommunities.test.ts b/backend/src/federation/validateCommunities.test.ts index 4f6339771..9ff8d545f 100644 --- a/backend/src/federation/validateCommunities.test.ts +++ b/backend/src/federation/validateCommunities.test.ts @@ -68,7 +68,7 @@ describe('validate Communities', () => { return { data: {} } as Response }) const variables1 = { - publicKey: Buffer.from('11111111111111111111111111111111'), + publicKey: Buffer.from('11111111111111111111111111111111', 'hex'), apiVersion: '1_0', endPoint: 'http//localhost:5001/api/', lastAnnouncedAt: new Date(), @@ -113,7 +113,7 @@ describe('validate Communities', () => { } as Response }) const variables1 = { - publicKey: Buffer.from('11111111111111111111111111111111'), + publicKey: Buffer.from('11111111111111111111111111111111', 'hex'), apiVersion: '1_0', endPoint: 'http//localhost:5001/api/', lastAnnouncedAt: new Date(), @@ -195,7 +195,7 @@ describe('validate Communities', () => { } as Response }) const variables1 = { - publicKey: Buffer.from('11111111111111111111111111111111'), + publicKey: Buffer.from('11111111111111111111111111111111', 'hex'), apiVersion: '1_0', endPoint: 'http//localhost:5001/api/', lastAnnouncedAt: new Date(), @@ -315,7 +315,7 @@ describe('validate Communities', () => { } as Response }) const variables3 = { - publicKey: Buffer.from('11111111111111111111111111111111'), + publicKey: Buffer.from('11111111111111111111111111111111', 'hex'), apiVersion: '2_0', endPoint: 'http//localhost:5001/api/', lastAnnouncedAt: new Date(), diff --git a/backend/src/federation/validateCommunities.ts b/backend/src/federation/validateCommunities.ts index 69b69070a..f19d606bd 100644 --- a/backend/src/federation/validateCommunities.ts +++ b/backend/src/federation/validateCommunities.ts @@ -3,14 +3,15 @@ import { IsNull } from '@dbTools/typeorm' import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' +import { FederatedCommunityLoggingView } from '@logging/FederatedCommunityLogging.view' -// eslint-disable-next-line camelcase import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient' import { PublicCommunityInfo } from '@/federation/client/1_0/model/PublicCommunityInfo' import { FederationClientFactory } from '@/federation/client/FederationClientFactory' import { backendLogger as logger } from '@/server/logger' import { startCommunityAuthentication } from './authenticateCommunities' +import { PublicCommunityInfoLoggingView } from './client/1_0/logging/PublicCommunityInfoLogging.view' import { ApiVersionType } from './enum/apiVersionType' export async function startValidateCommunities(timerInterval: number): Promise { @@ -37,7 +38,7 @@ export async function validateCommunities(): Promise { logger.debug(`Federation: found ${dbFederatedCommunities.length} dbCommunities`) for (const dbCom of dbFederatedCommunities) { - logger.debug('Federation: dbCom', dbCom) + logger.debug('Federation: dbCom', new FederatedCommunityLoggingView(dbCom)) const apiValueStrings: string[] = Object.values(ApiVersionType) logger.debug(`suppported ApiVersions=`, apiValueStrings) if (!apiValueStrings.includes(dbCom.apiVersion)) { @@ -53,7 +54,7 @@ export async function validateCommunities(): Promise { // eslint-disable-next-line camelcase if (client instanceof V1_0_FederationClient) { const pubKey = await client.getPublicKey() - if (pubKey && pubKey === dbCom.publicKey.toString()) { + if (pubKey && pubKey === dbCom.publicKey.toString('hex')) { await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() }) logger.debug(`Federation: verified community with:`, dbCom.endPoint) const pubComInfo = await client.getPublicCommunityInfo() @@ -68,7 +69,7 @@ export async function validateCommunities(): Promise { logger.debug( 'Federation: received not matching publicKey:', pubKey, - dbCom.publicKey.toString(), + dbCom.publicKey.toString('hex'), ) } } @@ -82,10 +83,11 @@ async function writeForeignCommunity( dbCom: DbFederatedCommunity, pubInfo: PublicCommunityInfo, ): Promise { - if (!dbCom || !pubInfo || !(dbCom.publicKey.toString() === pubInfo.publicKey)) { + if (!dbCom || !pubInfo || !(dbCom.publicKey.toString('hex') === pubInfo.publicKey)) { + const pubInfoView = new PublicCommunityInfoLoggingView(pubInfo) logger.error( - `Error in writeForeignCommunity: missmatching parameters or publicKey. pubInfo:${JSON.stringify( - pubInfo, + `Error in writeForeignCommunity: missmatching parameters or publicKey. pubInfo:${pubInfoView.toString( + true, )}`, ) } else { diff --git a/backend/src/graphql/arg/UserArgs.ts b/backend/src/graphql/arg/UserArgs.ts index 633ed5e20..406be14cb 100644 --- a/backend/src/graphql/arg/UserArgs.ts +++ b/backend/src/graphql/arg/UserArgs.ts @@ -3,11 +3,11 @@ import { ArgsType, Field } from 'type-graphql' @ArgsType() export class UserArgs { - @Field({ nullable: false }) + @Field() @IsString() identifier: string - @Field({ nullable: true }) + @Field() @IsString() - communityIdentifier?: string + communityIdentifier: string } diff --git a/backend/src/graphql/enum/GmsPublishLocationType.ts b/backend/src/graphql/enum/GmsPublishLocationType.ts new file mode 100644 index 000000000..afb9c246d --- /dev/null +++ b/backend/src/graphql/enum/GmsPublishLocationType.ts @@ -0,0 +1,12 @@ +import { registerEnumType } from 'type-graphql' + +export enum GmsPublishLocationType { + GMS_LOCATION_TYPE_EXACT = 0, + GMS_LOCATION_TYPE_APPROXIMATE = 1, + GMS_LOCATION_TYPE_RANDOM = 2, +} + +registerEnumType(GmsPublishLocationType, { + name: 'GmsPublishLocationType', // this one is mandatory + description: 'Type of location treatment in GMS', // this one is optional +}) diff --git a/backend/src/graphql/enum/GmsPublishNameType.ts b/backend/src/graphql/enum/GmsPublishNameType.ts new file mode 100644 index 000000000..08aaaf8ef --- /dev/null +++ b/backend/src/graphql/enum/GmsPublishNameType.ts @@ -0,0 +1,14 @@ +import { registerEnumType } from 'type-graphql' + +export enum GmsPublishNameType { + GMS_PUBLISH_NAME_ALIAS_OR_INITALS = 0, + GMS_PUBLISH_NAME_INITIALS = 1, + GMS_PUBLISH_NAME_FIRST = 2, + GMS_PUBLISH_NAME_FIRST_INITIAL = 3, + GMS_PUBLISH_NAME_FULL = 4, +} + +registerEnumType(GmsPublishNameType, { + name: 'GmsPublishNameType', // this one is mandatory + description: 'Type of name publishing', // this one is optional +}) diff --git a/backend/src/graphql/enum/GmsPublishPhoneType.ts b/backend/src/graphql/enum/GmsPublishPhoneType.ts new file mode 100644 index 000000000..a9821d8ff --- /dev/null +++ b/backend/src/graphql/enum/GmsPublishPhoneType.ts @@ -0,0 +1,12 @@ +import { registerEnumType } from 'type-graphql' + +export enum GmsPublishPhoneType { + GMS_PUBLISH_PHONE_NOTHING = 0, + GMS_PUBLISH_PHONE_COUNTRY = 1, + GMS_PUBLISH_PHONE_FULL = 2, +} + +registerEnumType(GmsPublishPhoneType, { + name: 'GmsPublishPhoneType', // this one is mandatory + description: 'Type of Phone publishing', // this one is optional +}) diff --git a/backend/src/graphql/model/FederatedCommunity.ts b/backend/src/graphql/model/FederatedCommunity.ts index 856a10d23..61ffd8f5b 100644 --- a/backend/src/graphql/model/FederatedCommunity.ts +++ b/backend/src/graphql/model/FederatedCommunity.ts @@ -6,7 +6,7 @@ export class FederatedCommunity { constructor(dbCom: DbFederatedCommunity) { this.id = dbCom.id this.foreign = dbCom.foreign - this.publicKey = dbCom.publicKey.toString() + this.publicKey = dbCom.publicKey.toString('hex') this.url = (dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/') + dbCom.apiVersion this.lastAnnouncedAt = dbCom.lastAnnouncedAt diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index cd188f49f..d24a717c4 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -10,6 +10,9 @@ export class User { this.id = user.id this.foreign = user.foreign this.communityUuid = user.communityUuid + if (user.community) { + this.communityName = user.community.name + } this.gradidoID = user.gradidoID this.alias = user.alias if (user.emailContact) { diff --git a/backend/src/graphql/resolver/CommunityResolver.test.ts b/backend/src/graphql/resolver/CommunityResolver.test.ts index 011670e87..e0cdc06fa 100644 --- a/backend/src/graphql/resolver/CommunityResolver.test.ts +++ b/backend/src/graphql/resolver/CommunityResolver.test.ts @@ -34,6 +34,60 @@ afterAll(async () => { await con.close() }) +// real valid ed25519 key pairs +const ed25519KeyPairStaticHex = [ + { + public: '264c1e88914d18166cc31e8d6c2111c03ac83f5910398eb45cd425c6c3836367', + private: + '0ddcafd5e2da92e171ccc974af22fee3ad8407475e330586c8f259837d4fedc6264c1e88914d18166cc31e8d6c2111c03ac83f5910398eb45cd425c6c3836367', + }, + { + public: 'ac18a8754f725079f93d27b9054f2eff536109a2fd439f9755941abdd639baf0', + private: + '45325a0d0f22655095321d9d05999c65245da02130318ff51da1ee423b836117ac18a8754f725079f93d27b9054f2eff536109a2fd439f9755941abdd639baf0', + }, + { + public: '6f7d4ccde610db1e1a33fabbb444d5400013c168296b03fd50bc686d4c1ad0ed', + private: + '8ab6d5da8b666ef5b3d754559c028806a1e2f8142a3e7ada411a8b6a3fe70eeb6f7d4ccde610db1e1a33fabbb444d5400013c168296b03fd50bc686d4c1ad0ed', + }, + { + public: '85fbbce0763db24677cf7cb579a743013557a4fea0a9a624245f3ae8cd785e1d', + private: + '0369ea7c80c3134c2872c3cf77a68f12d57de57359145b550e3a0c4c8170a31785fbbce0763db24677cf7cb579a743013557a4fea0a9a624245f3ae8cd785e1d', + }, + { + public: 'b099d023476ece01f231c269cbe496139ca73b3b4eb705816a511a1ca09661d0', + private: + '015ac650157b9e9bdbe718940606242daa318a251e8417b49440495e5afe3750b099d023476ece01f231c269cbe496139ca73b3b4eb705816a511a1ca09661d0', + }, + { + public: '9f8dc17f1af9f71e9b9a1cd49ca295b89049863515a487578ad4f90b307abf39', + private: + '0c13e71c55a3c03bd5df05c92bbccde88ad4a47f3bac6bdc5383ef1ec946cfdc9f8dc17f1af9f71e9b9a1cd49ca295b89049863515a487578ad4f90b307abf39', + }, + { + public: '34218b2f570d341370dd2db111d0ef2415c03a110c3bf3127c6b2337af71753a', + private: + '60f3479bba44d035886ac21c362bceece9f9ec81859c9b37f734b6442a06c93b34218b2f570d341370dd2db111d0ef2415c03a110c3bf3127c6b2337af71753a', + }, + { + public: 'a447404f5e04ed4896ed64d0f704574ed780b52e90868d4b83e1afb8ea687ff6', + private: + 'ea85ebb4332a52d87fe6f322dcd23ad4afc5eafb93dfff2216f3ffa9f0730e8aa447404f5e04ed4896ed64d0f704574ed780b52e90868d4b83e1afb8ea687ff6', + }, + { + public: 'b8b987c55da62b30d929672520551033eb37abdd88f9ea104db5d107c19680b4', + private: + '29475dbbc96d694b3c653a1e143caf084f6daf2d35267522c4096c55b47e2b76b8b987c55da62b30d929672520551033eb37abdd88f9ea104db5d107c19680b4', + }, + { + public: '40203d18a6ff8fb3c4c62d78e4807036fc9207782ce97a9bcf3be0755c236c37', + private: + '0b5c4d536d222e88b561ea495e15918fb8cba61a3f8c261ec9e587cca560804040203d18a6ff8fb3c4c62d78e4807036fc9207782ce97a9bcf3be0755c236c37', + }, +] + describe('CommunityResolver', () => { describe('getCommunities', () => { let homeCom1: DbFederatedCommunity @@ -62,7 +116,7 @@ describe('CommunityResolver', () => { homeCom1 = DbFederatedCommunity.create() homeCom1.foreign = false - homeCom1.publicKey = Buffer.from('publicKey-HomeCommunity') + homeCom1.publicKey = Buffer.from(ed25519KeyPairStaticHex[0].public, 'hex') homeCom1.apiVersion = '1_0' homeCom1.endPoint = 'http://localhost/api' homeCom1.createdAt = new Date() @@ -70,7 +124,7 @@ describe('CommunityResolver', () => { homeCom2 = DbFederatedCommunity.create() homeCom2.foreign = false - homeCom2.publicKey = Buffer.from('publicKey-HomeCommunity') + homeCom2.publicKey = Buffer.from(ed25519KeyPairStaticHex[1].public, 'hex') homeCom2.apiVersion = '1_1' homeCom2.endPoint = 'http://localhost/api' homeCom2.createdAt = new Date() @@ -78,7 +132,7 @@ describe('CommunityResolver', () => { homeCom3 = DbFederatedCommunity.create() homeCom3.foreign = false - homeCom3.publicKey = Buffer.from('publicKey-HomeCommunity') + homeCom3.publicKey = Buffer.from(ed25519KeyPairStaticHex[2].public, 'hex') homeCom3.apiVersion = '2_0' homeCom3.endPoint = 'http://localhost/api' homeCom3.createdAt = new Date() @@ -92,7 +146,7 @@ describe('CommunityResolver', () => { { id: 3, foreign: homeCom3.foreign, - publicKey: expect.stringMatching('publicKey-HomeCommunity'), + publicKey: expect.stringMatching(ed25519KeyPairStaticHex[2].public), url: expect.stringMatching('http://localhost/api/2_0'), lastAnnouncedAt: null, verifiedAt: null, @@ -103,7 +157,7 @@ describe('CommunityResolver', () => { { id: 2, foreign: homeCom2.foreign, - publicKey: expect.stringMatching('publicKey-HomeCommunity'), + publicKey: expect.stringMatching(ed25519KeyPairStaticHex[1].public), url: expect.stringMatching('http://localhost/api/1_1'), lastAnnouncedAt: null, verifiedAt: null, @@ -114,7 +168,7 @@ describe('CommunityResolver', () => { { id: 1, foreign: homeCom1.foreign, - publicKey: expect.stringMatching('publicKey-HomeCommunity'), + publicKey: expect.stringMatching(ed25519KeyPairStaticHex[0].public), url: expect.stringMatching('http://localhost/api/1_0'), lastAnnouncedAt: null, verifiedAt: null, @@ -134,7 +188,7 @@ describe('CommunityResolver', () => { foreignCom1 = DbFederatedCommunity.create() foreignCom1.foreign = true - foreignCom1.publicKey = Buffer.from('publicKey-ForeignCommunity') + foreignCom1.publicKey = Buffer.from(ed25519KeyPairStaticHex[3].public, 'hex') foreignCom1.apiVersion = '1_0' foreignCom1.endPoint = 'http://remotehost/api' foreignCom1.createdAt = new Date() @@ -142,7 +196,7 @@ describe('CommunityResolver', () => { foreignCom2 = DbFederatedCommunity.create() foreignCom2.foreign = true - foreignCom2.publicKey = Buffer.from('publicKey-ForeignCommunity') + foreignCom2.publicKey = Buffer.from(ed25519KeyPairStaticHex[4].public, 'hex') foreignCom2.apiVersion = '1_1' foreignCom2.endPoint = 'http://remotehost/api' foreignCom2.createdAt = new Date() @@ -150,7 +204,7 @@ describe('CommunityResolver', () => { foreignCom3 = DbFederatedCommunity.create() foreignCom3.foreign = true - foreignCom3.publicKey = Buffer.from('publicKey-ForeignCommunity') + foreignCom3.publicKey = Buffer.from(ed25519KeyPairStaticHex[5].public, 'hex') foreignCom3.apiVersion = '1_2' foreignCom3.endPoint = 'http://remotehost/api' foreignCom3.createdAt = new Date() @@ -164,7 +218,7 @@ describe('CommunityResolver', () => { { id: 3, foreign: homeCom3.foreign, - publicKey: expect.stringMatching('publicKey-HomeCommunity'), + publicKey: expect.stringMatching(ed25519KeyPairStaticHex[2].public), url: expect.stringMatching('http://localhost/api/2_0'), lastAnnouncedAt: null, verifiedAt: null, @@ -175,7 +229,7 @@ describe('CommunityResolver', () => { { id: 2, foreign: homeCom2.foreign, - publicKey: expect.stringMatching('publicKey-HomeCommunity'), + publicKey: expect.stringMatching(ed25519KeyPairStaticHex[1].public), url: expect.stringMatching('http://localhost/api/1_1'), lastAnnouncedAt: null, verifiedAt: null, @@ -186,7 +240,7 @@ describe('CommunityResolver', () => { { id: 1, foreign: homeCom1.foreign, - publicKey: expect.stringMatching('publicKey-HomeCommunity'), + publicKey: expect.stringMatching(ed25519KeyPairStaticHex[0].public), url: expect.stringMatching('http://localhost/api/1_0'), lastAnnouncedAt: null, verifiedAt: null, @@ -197,7 +251,7 @@ describe('CommunityResolver', () => { { id: 6, foreign: foreignCom3.foreign, - publicKey: expect.stringMatching('publicKey-ForeignCommunity'), + publicKey: expect.stringMatching(ed25519KeyPairStaticHex[5].public), url: expect.stringMatching('http://remotehost/api/1_2'), lastAnnouncedAt: null, verifiedAt: null, @@ -208,7 +262,7 @@ describe('CommunityResolver', () => { { id: 5, foreign: foreignCom2.foreign, - publicKey: expect.stringMatching('publicKey-ForeignCommunity'), + publicKey: expect.stringMatching(ed25519KeyPairStaticHex[4].public), url: expect.stringMatching('http://remotehost/api/1_1'), lastAnnouncedAt: null, verifiedAt: null, @@ -219,7 +273,7 @@ describe('CommunityResolver', () => { { id: 4, foreign: foreignCom1.foreign, - publicKey: expect.stringMatching('publicKey-ForeignCommunity'), + publicKey: expect.stringMatching(ed25519KeyPairStaticHex[3].public), url: expect.stringMatching('http://remotehost/api/1_0'), lastAnnouncedAt: null, verifiedAt: null, @@ -264,8 +318,8 @@ describe('CommunityResolver', () => { homeCom1 = DbCommunity.create() homeCom1.foreign = false homeCom1.url = 'http://localhost/api' - homeCom1.publicKey = Buffer.from('publicKey-HomeCommunity') - homeCom1.privateKey = Buffer.from('privateKey-HomeCommunity') + homeCom1.publicKey = Buffer.from(ed25519KeyPairStaticHex[0].public, 'hex') + homeCom1.privateKey = Buffer.from(ed25519KeyPairStaticHex[0].private, 'hex') homeCom1.communityUuid = 'HomeCom-UUID' homeCom1.authenticatedAt = new Date() homeCom1.name = 'HomeCommunity-name' @@ -302,8 +356,8 @@ describe('CommunityResolver', () => { homeCom1 = DbCommunity.create() homeCom1.foreign = false homeCom1.url = 'http://localhost/api' - homeCom1.publicKey = Buffer.from('publicKey-HomeCommunity') - homeCom1.privateKey = Buffer.from('privateKey-HomeCommunity') + homeCom1.publicKey = Buffer.from(ed25519KeyPairStaticHex[0].public, 'hex') + homeCom1.privateKey = Buffer.from(ed25519KeyPairStaticHex[0].private, 'hex') homeCom1.communityUuid = 'HomeCom-UUID' homeCom1.authenticatedAt = new Date() homeCom1.name = 'HomeCommunity-name' @@ -314,8 +368,8 @@ 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(ed25519KeyPairStaticHex[3].public, 'hex') + foreignCom1.privateKey = Buffer.from(ed25519KeyPairStaticHex[3].private, 'hex') // foreignCom1.communityUuid = 'Stage2-Com-UUID' // foreignCom1.authenticatedAt = new Date() foreignCom1.name = 'Stage-2_Community-name' @@ -326,8 +380,8 @@ 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(ed25519KeyPairStaticHex[4].public, 'hex') + foreignCom2.privateKey = Buffer.from(ed25519KeyPairStaticHex[4].private, 'hex') foreignCom2.communityUuid = 'Stage3-Com-UUID' foreignCom2.authenticatedAt = new Date() foreignCom2.name = 'Stage-3_Community-name' diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 8b2bf141e..a188c5d2c 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -2609,7 +2609,7 @@ describe('ContributionResolver', () => { expect(transaction[0].linkedTransactionId).toEqual(null) expect(transaction[0].transactionLinkId).toEqual(null) expect(transaction[0].previous).toEqual(null) - expect(transaction[0].linkedUserId).toEqual(null) + expect(transaction[0].linkedUserId).toEqual(admin.id) expect(transaction[0].typeId).toEqual(1) }) diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index bd985d2d2..5684835e4 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -451,6 +451,11 @@ export class ContributionResolver { transaction.userId = contribution.userId transaction.userGradidoID = user.gradidoID transaction.userName = fullName(user.firstName, user.lastName) + transaction.userCommunityUuid = user.communityUuid + transaction.linkedUserId = moderatorUser.id + transaction.linkedUserGradidoID = moderatorUser.gradidoID + transaction.linkedUserName = fullName(moderatorUser.firstName, moderatorUser.lastName) + transaction.linkedUserCommunityUuid = moderatorUser.communityUuid transaction.previous = lastTransaction ? lastTransaction.id : null transaction.amount = contribution.amount transaction.creationDate = contribution.contributionDate diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index d130a802e..97e210dfa 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -142,7 +142,11 @@ describe('send coins', () => { }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('No user with this credentials', 'wrong@email.com') + expect(logger.error).toBeCalledWith( + 'No user with this credentials', + 'wrong@email.com', + homeCom.communityUuid, + ) }) describe('deleted recipient', () => { @@ -165,13 +169,17 @@ describe('send coins', () => { }), ).toEqual( expect.objectContaining({ - errors: [new GraphQLError('No user to given contact')], + errors: [new GraphQLError('No user with this credentials')], }), ) }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('No user to given contact', 'stephen@hawking.uk') + expect(logger.error).toBeCalledWith( + 'No user with this credentials', + 'stephen@hawking.uk', + homeCom.communityUuid, + ) }) }) @@ -204,6 +212,7 @@ describe('send coins', () => { expect(logger.error).toBeCalledWith( 'No user with this credentials', 'garrick@ollivander.com', + homeCom.communityUuid, ) }) }) diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 8d35708a6..00894ecd3 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -109,9 +109,11 @@ export const executeTransaction = async ( transactionSend.userId = sender.id transactionSend.userGradidoID = sender.gradidoID transactionSend.userName = fullName(sender.firstName, sender.lastName) + transactionSend.userCommunityUuid = sender.communityUuid transactionSend.linkedUserId = recipient.id transactionSend.linkedUserGradidoID = recipient.gradidoID transactionSend.linkedUserName = fullName(recipient.firstName, recipient.lastName) + transactionSend.linkedUserCommunityUuid = recipient.communityUuid transactionSend.amount = amount.mul(-1) transactionSend.balance = sendBalance.balance transactionSend.balanceDate = receivedCallDate @@ -129,9 +131,11 @@ export const executeTransaction = async ( transactionReceive.userId = recipient.id transactionReceive.userGradidoID = recipient.gradidoID transactionReceive.userName = fullName(recipient.firstName, recipient.lastName) + transactionReceive.userCommunityUuid = recipient.communityUuid transactionReceive.linkedUserId = sender.id transactionReceive.linkedUserGradidoID = sender.gradidoID transactionReceive.linkedUserName = fullName(sender.firstName, sender.lastName) + transactionReceive.linkedUserCommunityUuid = sender.communityUuid transactionReceive.amount = amount const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate) transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount @@ -254,6 +258,9 @@ export class TransactionResolver { // userTransactions.forEach((transaction: dbTransaction) => { // use normal for loop because of timing problems with await in forEach-loop for (const transaction of userTransactions) { + if (transaction.typeId === TransactionTypeId.CREATION) { + continue + } if (transaction.linkedUserId && !involvedUserIds.includes(transaction.linkedUserId)) { involvedUserIds.push(transaction.linkedUserId) } @@ -425,7 +432,7 @@ export class TransactionResolver { const senderUser = getUser(context) if (!recipientCommunityIdentifier || (await isHomeCommunity(recipientCommunityIdentifier))) { - // processing sendCoins within sender and recepient are both in home community + // processing sendCoins within sender and recipient are both in home community const recipientUser = await findUserByIdentifier( recipientIdentifier, recipientCommunityIdentifier, diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index a4892496b..9bb15c66e 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -2561,6 +2561,7 @@ describe('UserResolver', () => { query: userQuery, variables: { identifier: 'identifier', + communityIdentifier: 'community identifier', }, }), ).resolves.toEqual( @@ -2646,13 +2647,11 @@ describe('UserResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [ - new GraphQLError('Found user to given contact, but belongs to other community'), - ], + errors: [new GraphQLError('No user with this credentials')], }), ) expect(logger.error).toBeCalledWith( - 'Found user to given contact, but belongs to other community', + 'No user with this credentials', 'bibi@bloxberg.de', foreignCom1.communityUuid, ) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 45053bda4..7be858d7a 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -65,7 +65,7 @@ import random from 'random-bigint' import { randombytes_random } from 'sodium-native' import { FULL_CREATION_AVAILABLE } from './const/const' -import { getCommunityName, getHomeCommunity } from './util/communities' +import { getHomeCommunity } from './util/communities' import { getUserCreations } from './util/creations' import { findUserByIdentifier } from './util/findUserByIdentifier' import { findUsers } from './util/findUsers' @@ -834,11 +834,6 @@ export class UserResolver { ): Promise { const foundDbUser = await findUserByIdentifier(identifier, communityIdentifier) const modelUser = new User(foundDbUser) - if (!foundDbUser.communityUuid) { - modelUser.communityName = (await Promise.resolve(getHomeCommunity())).name - } else { - modelUser.communityName = await getCommunityName(foundDbUser.communityUuid) - } return modelUser } } diff --git a/backend/src/graphql/resolver/semaphore.test.ts b/backend/src/graphql/resolver/semaphore.test.ts index dc6c5b364..ed58d55ec 100644 --- a/backend/src/graphql/resolver/semaphore.test.ts +++ b/backend/src/graphql/resolver/semaphore.test.ts @@ -6,6 +6,7 @@ import { Community as DbCommunity } from '@entity/Community' import { ApolloServerTestClient } from 'apollo-server-testing' import { Decimal } from 'decimal.js-light' import { GraphQLError } from 'graphql' +import { v4 as uuidv4 } from 'uuid' import { cleanDB, testEnvironment, contributionDateFormatter } from '@test/helpers' @@ -54,7 +55,7 @@ describe('semaphore', () => { beforeAll(async () => { const now = new Date() homeCom = DbCommunity.create() - homeCom.communityUuid = 'homeCom-UUID' + homeCom.communityUuid = uuidv4() homeCom.creationDate = new Date('2000-01-01') homeCom.description = 'homeCom description' homeCom.foreign = false diff --git a/backend/src/graphql/resolver/util/findUserByIdentifier.ts b/backend/src/graphql/resolver/util/findUserByIdentifier.ts index 6b7f1bccc..7e52327d3 100644 --- a/backend/src/graphql/resolver/util/findUserByIdentifier.ts +++ b/backend/src/graphql/resolver/util/findUserByIdentifier.ts @@ -1,3 +1,5 @@ +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 { validate, version } from 'uuid' @@ -6,15 +8,26 @@ import { LogError } from '@/server/LogError' import { VALID_ALIAS_REGEX } from './validateAlias' +/** + * + * @param identifier could be gradidoID, alias or email of user + * @param communityIdentifier could be uuid or name of community + * @returns + */ export const findUserByIdentifier = async ( identifier: string, - communityIdentifier?: string, + communityIdentifier: string, ): Promise => { let user: DbUser | null + const communityWhere: FindOptionsWhere = + validate(communityIdentifier) && version(communityIdentifier) === 4 + ? { communityUuid: communityIdentifier } + : { name: communityIdentifier } + if (validate(identifier) && version(identifier) === 4) { user = await DbUser.findOne({ - where: { gradidoID: identifier, communityUuid: communityIdentifier }, - relations: ['emailContact'], + where: { gradidoID: identifier, community: communityWhere }, + relations: ['emailContact', 'community'], }) if (!user) { throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier) @@ -24,28 +37,21 @@ export const findUserByIdentifier = async ( where: { email: identifier, emailChecked: true, + user: { + community: communityWhere, + }, }, - relations: ['user'], + relations: { user: { community: true } }, }) if (!userContact) { - throw new LogError('No user with this credentials', identifier) - } - if (!userContact.user) { - throw new LogError('No user to given contact', identifier) - } - if (userContact.user.communityUuid !== communityIdentifier) { - throw new LogError( - 'Found user to given contact, but belongs to other community', - identifier, - communityIdentifier, - ) + throw new LogError('No user with this credentials', identifier, communityIdentifier) } user = userContact.user user.emailContact = userContact } else if (VALID_ALIAS_REGEX.exec(identifier)) { user = await DbUser.findOne({ - where: { alias: identifier, communityUuid: communityIdentifier }, - relations: ['emailContact'], + where: { alias: identifier, community: communityWhere }, + relations: ['emailContact', 'community'], }) if (!user) { throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier) diff --git a/backend/src/graphql/resolver/util/findUserByIdentifiers.test.ts b/backend/src/graphql/resolver/util/findUserByIdentifiers.test.ts new file mode 100644 index 000000000..cfdbce8f2 --- /dev/null +++ b/backend/src/graphql/resolver/util/findUserByIdentifiers.test.ts @@ -0,0 +1,94 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +import { Connection } from '@dbTools/typeorm' +import { Community as DbCommunity } from '@entity/Community' +import { User as DbUser } from '@entity/User' +import { ApolloServerTestClient } from 'apollo-server-testing' + +import { cleanDB, testEnvironment } from '@test/helpers' + +import { writeHomeCommunityEntry } from '@/seeds/community' +import { userFactory } from '@/seeds/factory/user' +import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' +import { bobBaumeister } from '@/seeds/users/bob-baumeister' +import { peterLustig } from '@/seeds/users/peter-lustig' + +import { findUserByIdentifier } from './findUserByIdentifier' + +let con: Connection +let testEnv: { + mutate: ApolloServerTestClient['mutate'] + query: ApolloServerTestClient['query'] + con: Connection +} + +beforeAll(async () => { + testEnv = await testEnvironment() + con = testEnv.con + await cleanDB() +}) + +afterAll(async () => { + await cleanDB() + await con.close() +}) + +describe('graphql/resolver/util/findUserByIdentifier', () => { + let homeCom: DbCommunity + let communityUuid: string + let communityName: string + let userBibi: DbUser + + beforeAll(async () => { + homeCom = await writeHomeCommunityEntry() + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + communityUuid = homeCom.communityUuid! + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + communityName = homeCom.communityUuid! + + userBibi = await userFactory(testEnv, bibiBloxberg) + await userFactory(testEnv, peterLustig) + await userFactory(testEnv, bobBaumeister) + }) + + describe('communityIdentifier is community uuid', () => { + it('userIdentifier is gradido id', async () => { + const user = await findUserByIdentifier(userBibi.gradidoID, communityUuid) + user.userRoles = [] + expect(user).toMatchObject(userBibi) + }) + + it('userIdentifier is alias', async () => { + const user = await findUserByIdentifier(userBibi.alias, communityUuid) + user.userRoles = [] + expect(user).toMatchObject(userBibi) + }) + + it('userIdentifier is email', async () => { + const user = await findUserByIdentifier(userBibi.emailContact.email, communityUuid) + user.userRoles = [] + expect(user).toMatchObject(userBibi) + }) + }) + + describe('communityIdentifier is community name', () => { + it('userIdentifier is gradido id', async () => { + const user = await findUserByIdentifier(userBibi.gradidoID, communityName) + user.userRoles = [] + expect(user).toMatchObject(userBibi) + }) + + it('userIdentifier is alias', async () => { + const user = await findUserByIdentifier(userBibi.alias, communityName) + user.userRoles = [] + expect(user).toMatchObject(userBibi) + }) + + it('userIdentifier is email', async () => { + const user = await findUserByIdentifier(userBibi.emailContact.email, communityName) + user.userRoles = [] + expect(user).toMatchObject(userBibi) + }) + }) +}) diff --git a/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.test.ts b/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.test.ts index d9a2da569..0d85a35af 100644 --- a/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.test.ts +++ b/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.test.ts @@ -22,6 +22,13 @@ import { logger, i18n as localization } from '@test/testSetup' import { CONFIG } from '@/config' import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId' +import { creations } from '@/seeds/creation' +import { creationFactory } from '@/seeds/factory/creation' +import { userFactory } from '@/seeds/factory/user' +import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' +import { bobBaumeister } from '@/seeds/users/bob-baumeister' +import { peterLustig } from '@/seeds/users/peter-lustig' +import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz' import { sendTransactionsToDltConnector } from './sendTransactionsToDltConnector' @@ -423,9 +430,17 @@ describe('create and send Transactions to DltConnector', () => { describe('with 3 creations and active dlt-connector', () => { it('found 3 dlt-transactions', async () => { - txCREATION1 = await createTxCREATION1(false) - txCREATION2 = await createTxCREATION2(false) - txCREATION3 = await createTxCREATION3(false) + await userFactory(testEnv, bibiBloxberg) + await userFactory(testEnv, peterLustig) + await userFactory(testEnv, raeuberHotzenplotz) + await userFactory(testEnv, bobBaumeister) + let count = 0 + for (const creation of creations) { + await creationFactory(testEnv, creation) + count++ + // we need only 3 for testing + if (count >= 3) break + } await createHomeCommunity() CONFIG.DLT_CONNECTOR = true @@ -435,10 +450,7 @@ describe('create and send Transactions to DltConnector', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { data: { - sendTransaction: { - dltTransactionIdHex: - '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621', - }, + sendTransaction: { succeed: true }, }, } as Response }) @@ -464,7 +476,7 @@ describe('create and send Transactions to DltConnector', () => { expect.objectContaining({ id: expect.any(Number), transactionId: transactions[0].id, - messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621', + messageId: 'sended', verified: false, createdAt: expect.any(Date), verifiedAt: null, @@ -472,7 +484,7 @@ describe('create and send Transactions to DltConnector', () => { expect.objectContaining({ id: expect.any(Number), transactionId: transactions[1].id, - messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621', + messageId: 'sended', verified: false, createdAt: expect.any(Date), verifiedAt: null, @@ -480,7 +492,7 @@ describe('create and send Transactions to DltConnector', () => { expect.objectContaining({ id: expect.any(Number), transactionId: transactions[2].id, - messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621', + messageId: 'sended', verified: false, createdAt: expect.any(Date), verifiedAt: null, @@ -514,10 +526,7 @@ describe('create and send Transactions to DltConnector', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return { data: { - sendTransaction: { - dltTransactionIdHex: - '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621', - }, + sendTransaction: { succeed: true }, }, } as Response }) @@ -569,7 +578,7 @@ describe('create and send Transactions to DltConnector', () => { expect.objectContaining({ id: expect.any(Number), transactionId: txSEND1to2.id, - messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621', + messageId: 'sended', verified: false, createdAt: expect.any(Date), verifiedAt: null, @@ -577,7 +586,7 @@ describe('create and send Transactions to DltConnector', () => { expect.objectContaining({ id: expect.any(Number), transactionId: txRECEIVE2From1.id, - messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621', + messageId: 'sended', verified: false, createdAt: expect.any(Date), verifiedAt: null, diff --git a/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.ts b/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.ts index 98e1ffbe3..733c12594 100644 --- a/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.ts +++ b/backend/src/graphql/resolver/util/sendTransactionsToDltConnector.ts @@ -1,9 +1,9 @@ import { IsNull } from '@dbTools/typeorm' -import { Community } from '@entity/Community' import { DltTransaction } from '@entity/DltTransaction' import { Transaction } from '@entity/Transaction' -import { DltConnectorClient } from '@/apis/DltConnectorClient' +import { DltConnectorClient } from '@dltConnector/DltConnectorClient' + import { backendLogger as logger } from '@/server/logger' import { Monitor, MonitorNames } from '@/util/Monitor' @@ -17,13 +17,6 @@ export async function sendTransactionsToDltConnector(): Promise { try { await createDltTransactions() const dltConnector = DltConnectorClient.getInstance() - // TODO: get actual communities from users - const homeCommunity = await Community.findOneOrFail({ where: { foreign: false } }) - const senderCommunityUuid = homeCommunity.communityUuid - if (!senderCommunityUuid) { - throw new Error('Cannot find community uuid of home community') - } - const recipientCommunityUuid = '' if (dltConnector) { logger.debug('with sending to DltConnector...') const dltTransactions = await DltTransaction.find({ @@ -37,22 +30,14 @@ export async function sendTransactionsToDltConnector(): Promise { continue } try { - const messageId = await dltConnector.transmitTransaction( - dltTx.transaction, - senderCommunityUuid, - recipientCommunityUuid, - ) - const dltMessageId = Buffer.from(messageId, 'hex') - if (dltMessageId.length !== 32) { - logger.error( - 'Error dlt message id is invalid: %s, should by 32 Bytes long in binary after converting from hex', - dltMessageId, - ) - return + const result = await dltConnector.transmitTransaction(dltTx.transaction) + // message id isn't known at this point of time, because transaction will not direct sended to iota, + // it will first go to db and then sended, if no transaction is in db before + if (result) { + dltTx.messageId = 'sended' + await DltTransaction.save(dltTx) + logger.info('store messageId=%s in dltTx=%d', dltTx.messageId, dltTx.id) } - dltTx.messageId = dltMessageId.toString('hex') - await DltTransaction.save(dltTx) - logger.info('store messageId=%s in dltTx=%d', dltTx.messageId, dltTx.id) } catch (e) { logger.error( `error while sending to dlt-connector or writing messageId of dltTx=${dltTx.id}`, diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 4bd5008d3..2daa5e8bd 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -370,7 +370,7 @@ export const adminListContributionMessages = gql` ` export const user = gql` - query ($identifier: String!, $communityIdentifier: String) { + query ($identifier: String!, $communityIdentifier: String!) { user(identifier: $identifier, communityIdentifier: $communityIdentifier) { firstName lastName diff --git a/backend/src/seeds/users/bibi-bloxberg.ts b/backend/src/seeds/users/bibi-bloxberg.ts index 7c372848e..9a40e922b 100644 --- a/backend/src/seeds/users/bibi-bloxberg.ts +++ b/backend/src/seeds/users/bibi-bloxberg.ts @@ -4,6 +4,7 @@ export const bibiBloxberg: UserInterface = { email: 'bibi@bloxberg.de', firstName: 'Bibi', lastName: 'Bloxberg', + alias: 'BBB', // description: 'Hex Hex', emailChecked: true, language: 'de', diff --git a/backend/src/server/createServer.ts b/backend/src/server/createServer.ts index c162d9f6f..250a4b901 100644 --- a/backend/src/server/createServer.ts +++ b/backend/src/server/createServer.ts @@ -4,6 +4,8 @@ import { Connection as DbConnection } from '@dbTools/typeorm' import { ApolloServer } from 'apollo-server-express' import express, { Express, json, urlencoded } from 'express' +import { slowDown } from 'express-slow-down' +import helmet from 'helmet' import { Logger } from 'log4js' import { CONFIG } from '@/config' @@ -56,6 +58,28 @@ export const createServer = async ( // cors app.use(cors) + // Helmet helps secure Express apps by setting HTTP response headers. + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + app.use(helmet()) + + // rate limiter/ slow down to many requests + const limiter = slowDown({ + windowMs: 1000, // 1 second + delayAfter: 10, // Allow 10 requests per 1 second. + delayMs: (hits) => hits * 50, // Add 100 ms of delay to every request after the 10th one. + /** + * So: + * + * - requests 1-10 are not delayed. + * - request 11 is delayed by 550ms + * - request 12 is delayed by 600ms + * - request 13 is delayed by 650ms + * + * and so on. After 1 seconds, the delay is reset to 0. + */ + }) + app.use(limiter) + // bodyparser json app.use(json()) // bodyparser urlencoded for elopage diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index 9e337dfe7..732c585d0 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -50,6 +50,7 @@ const communityDbUser: dbUser = { }, foreign: false, communityUuid: '55555555-4444-4333-2222-11111111', + community: null, gmsPublishName: 0, gmsAllowed: false, location: null, diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 6d27ca0fa..28ddf1c38 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/*"], + "@dltConnector/*": ["src/apis/dltConnector/*"], "@enum/*": ["src/graphql/enum/*"], "@model/*": ["src/graphql/model/*"], "@union/*": ["src/graphql/union/*"], @@ -57,7 +58,8 @@ "@test/*": ["test/*"], /* external */ "@dbTools/*": ["../database/src/*", "../../database/build/src/*"], - "@entity/*": ["../database/entity/*", "../../database/build/entity/*"] + "@entity/*": ["../database/entity/*", "../../database/build/entity/*"], + "@logging/*": ["../database/logging/*", "../../database/build/logging/*"] }, // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ "typeRoots": ["@types", "node_modules/@types"], /* List of folders to include type definitions from. */ diff --git a/backend/yarn.lock b/backend/yarn.lock index 0b3ceb323..234dc817a 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -3225,6 +3225,18 @@ expect@^27.2.5: jest-message-util "^27.2.5" jest-regex-util "^27.0.6" +express-rate-limit@7: + version "7.1.5" + resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-7.1.5.tgz#af4c81143a945ea97f2599d13957440a0ddbfcfe" + integrity sha512-/iVogxu7ueadrepw1bS0X0kaRC/U0afwiYRSLg68Ts+p4Dc85Q5QKsOnPS/QUjPMHvOJQtBDrZgvkOzf8ejUYw== + +express-slow-down@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/express-slow-down/-/express-slow-down-2.0.1.tgz#60c4515467314675d89c54ec608e2d586aa30f87" + integrity sha512-zRogSZhNXJYKDBekhgFfFXGrOngH7Fub7Mx2g8OQ4RUBwSJP/3TVEKMgSGR/WlneT0mJ6NBUnidHhIELGVPe3w== + dependencies: + express-rate-limit "7" + express@^4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" @@ -3679,7 +3691,7 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0: integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== "gradido-database@file:../database": - version "2.0.1" + version "2.1.1" dependencies: "@types/uuid" "^8.3.4" cross-env "^7.0.3" @@ -3826,6 +3838,11 @@ he@1.2.0, he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +helmet@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-5.1.1.tgz#609823c5c2e78aea62dd9afc8f544ca409da5e85" + integrity sha512-/yX0oVZBggA9cLJh8aw3PPCfedBnbd7J2aowjzsaWwZh7/UFY0nccn/aHAggIgWUFfnykX8GKd3a1pSbrmlcVQ== + highlight.js@^10.7.1: version "10.7.3" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" diff --git a/database/entity/0081-user_join_community/Community.ts b/database/entity/0081-user_join_community/Community.ts new file mode 100644 index 000000000..1c6b36be3 --- /dev/null +++ b/database/entity/0081-user_join_community/Community.ts @@ -0,0 +1,70 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + OneToMany, + JoinColumn, +} from 'typeorm' +import { User } from '../User' + +@Entity('communities') +export class Community extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ name: 'foreign', type: 'bool', nullable: false, default: true }) + foreign: boolean + + @Column({ name: 'url', length: 255, nullable: false }) + url: string + + @Column({ name: 'public_key', type: 'binary', length: 32, nullable: false }) + publicKey: Buffer + + @Column({ name: 'private_key', type: 'binary', length: 64, nullable: true }) + privateKey: Buffer | null + + @Column({ + name: 'community_uuid', + type: 'char', + length: 36, + nullable: true, + collation: 'utf8mb4_unicode_ci', + }) + communityUuid: string | null + + @Column({ name: 'authenticated_at', type: 'datetime', nullable: true }) + authenticatedAt: Date | null + + @Column({ name: 'name', type: 'varchar', length: 40, nullable: true }) + name: string | null + + @Column({ name: 'description', type: 'varchar', length: 255, nullable: true }) + description: string | null + + @CreateDateColumn({ name: 'creation_date', type: 'datetime', nullable: true }) + creationDate: Date | null + + @CreateDateColumn({ + name: 'created_at', + type: 'datetime', + default: () => 'CURRENT_TIMESTAMP(3)', + nullable: false, + }) + createdAt: Date + + @UpdateDateColumn({ + name: 'updated_at', + type: 'datetime', + onUpdate: 'CURRENT_TIMESTAMP(3)', + nullable: true, + }) + updatedAt: Date | null + + @OneToMany(() => User, (user) => user.community) + @JoinColumn({ name: 'community_uuid', referencedColumnName: 'communityUuid' }) + users: User[] +} diff --git a/database/entity/0081-user_join_community/User.ts b/database/entity/0081-user_join_community/User.ts new file mode 100644 index 000000000..28141029d --- /dev/null +++ b/database/entity/0081-user_join_community/User.ts @@ -0,0 +1,138 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + DeleteDateColumn, + OneToMany, + JoinColumn, + OneToOne, + ManyToOne, +} from 'typeorm' +import { Contribution } from '../Contribution' +import { ContributionMessage } from '../ContributionMessage' +import { UserContact } from '../UserContact' +import { UserRole } from '../UserRole' +import { Community } from '../Community' + +@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) +export class User extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ type: 'bool', default: false }) + foreign: boolean + + @Column({ + name: 'gradido_id', + length: 36, + nullable: false, + collation: 'utf8mb4_unicode_ci', + }) + gradidoID: string + + @Column({ + name: 'community_uuid', + type: 'char', + length: 36, + nullable: true, + collation: 'utf8mb4_unicode_ci', + }) + communityUuid: string + + @ManyToOne(() => Community, (community) => community.users) + @JoinColumn({ name: 'community_uuid', referencedColumnName: 'communityUuid' }) + community: Community | null + + @Column({ + name: 'alias', + length: 20, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + alias: string + + @OneToOne(() => UserContact, (emailContact: UserContact) => emailContact.user) + @JoinColumn({ name: 'email_id' }) + emailContact: UserContact + + @Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null }) + emailId: number | null + + @Column({ + name: 'first_name', + length: 255, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + firstName: string + + @Column({ + name: 'last_name', + length: 255, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + lastName: string + + @Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP(3)', nullable: false }) + createdAt: Date + + @DeleteDateColumn({ name: 'deleted_at', nullable: true }) + deletedAt: Date | null + + @Column({ type: 'bigint', default: 0, unsigned: true }) + password: BigInt + + @Column({ + name: 'password_encryption_type', + type: 'int', + unsigned: true, + nullable: false, + default: 0, + }) + passwordEncryptionType: number + + @Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false }) + language: string + + @Column({ type: 'bool', default: false }) + hideAmountGDD: boolean + + @Column({ type: 'bool', default: false }) + hideAmountGDT: boolean + + @OneToMany(() => UserRole, (userRole) => userRole.user) + @JoinColumn({ name: 'user_id' }) + userRoles: UserRole[] + + @Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null }) + referrerId?: number | null + + @Column({ + name: 'contribution_link_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + contributionLinkId?: number | null + + @Column({ name: 'publisher_id', default: 0 }) + publisherId: number + + @OneToMany(() => Contribution, (contribution) => contribution.user) + @JoinColumn({ name: 'user_id' }) + contributions?: Contribution[] + + @OneToMany(() => ContributionMessage, (message) => message.user) + @JoinColumn({ name: 'user_id' }) + messages?: ContributionMessage[] + + @OneToMany(() => UserContact, (userContact: UserContact) => userContact.user) + @JoinColumn({ name: 'user_id' }) + userContacts?: UserContact[] +} diff --git a/database/entity/0079-introduce_gms_registration/Community.ts b/database/entity/0082-introduce_gms_registration/Community.ts similarity index 88% rename from database/entity/0079-introduce_gms_registration/Community.ts rename to database/entity/0082-introduce_gms_registration/Community.ts index 89c31763f..cc5607e2e 100644 --- a/database/entity/0079-introduce_gms_registration/Community.ts +++ b/database/entity/0082-introduce_gms_registration/Community.ts @@ -5,7 +5,10 @@ import { Column, CreateDateColumn, UpdateDateColumn, + OneToMany, + JoinColumn, } from 'typeorm' +import { User } from '../User' @Entity('communities') export class Community extends BaseEntity { @@ -63,4 +66,8 @@ export class Community extends BaseEntity { nullable: true, }) updatedAt: Date | null + + @OneToMany(() => User, (user) => user.community) + @JoinColumn({ name: 'community_uuid', referencedColumnName: 'communityUuid' }) + users: User[] } diff --git a/database/entity/0079-introduce_gms_registration/User.ts b/database/entity/0082-introduce_gms_registration/User.ts similarity index 94% rename from database/entity/0079-introduce_gms_registration/User.ts rename to database/entity/0082-introduce_gms_registration/User.ts index f91129f31..e11842960 100644 --- a/database/entity/0079-introduce_gms_registration/User.ts +++ b/database/entity/0082-introduce_gms_registration/User.ts @@ -8,11 +8,13 @@ import { JoinColumn, OneToOne, Geometry, + ManyToOne, } from 'typeorm' import { Contribution } from '../Contribution' import { ContributionMessage } from '../ContributionMessage' import { UserContact } from '../UserContact' import { UserRole } from '../UserRole' +import { Community } from '../Community' @Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) export class User extends BaseEntity { @@ -39,6 +41,10 @@ export class User extends BaseEntity { }) communityUuid: string + @ManyToOne(() => Community, (community) => community.users) + @JoinColumn({ name: 'community_uuid', referencedColumnName: 'communityUuid' }) + community: Community | null + @Column({ name: 'alias', length: 20, diff --git a/database/entity/0079-introduce_gms_registration/UserContact.ts b/database/entity/0082-introduce_gms_registration/UserContact.ts similarity index 100% rename from database/entity/0079-introduce_gms_registration/UserContact.ts rename to database/entity/0082-introduce_gms_registration/UserContact.ts diff --git a/database/entity/Community.ts b/database/entity/Community.ts index d20d2d8d7..3b48d5c29 100644 --- a/database/entity/Community.ts +++ b/database/entity/Community.ts @@ -1 +1 @@ -export { Community } from './0079-introduce_gms_registration/Community' +export { Community } from './0082-introduce_gms_registration/Community' diff --git a/database/entity/User.ts b/database/entity/User.ts index 732d11e8e..e3f15113d 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0079-introduce_gms_registration/User' +export { User } from './0082-introduce_gms_registration/User' diff --git a/database/entity/UserContact.ts b/database/entity/UserContact.ts index 795ce0a43..e91e9a9d3 100644 --- a/database/entity/UserContact.ts +++ b/database/entity/UserContact.ts @@ -1 +1 @@ -export { UserContact } from './0079-introduce_gms_registration/UserContact' +export { UserContact } from './0082-introduce_gms_registration/UserContact' diff --git a/database/logging/AbstractLogging.view.ts b/database/logging/AbstractLogging.view.ts new file mode 100644 index 000000000..e51f3823d --- /dev/null +++ b/database/logging/AbstractLogging.view.ts @@ -0,0 +1,38 @@ +import util from 'util' + +import { Decimal } from 'decimal.js-light' + +export abstract class AbstractLoggingView { + // eslint-disable-next-line no-undef + protected bufferStringFormat: BufferEncoding = 'hex' + + // This function gets called automatically when JSON.stringify() is called on this class instance + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public abstract toJSON(): any + public toString(compact = false): string { + if (compact) { + return JSON.stringify(this.toJSON()) + } else { + return JSON.stringify(this.toJSON(), null, 2) + } + } + + // called form console.log or log4js logging functions + [util.inspect.custom](): string { + return this.toString() + } + + public dateToString(date: Date | undefined | null): string | undefined { + if (date) { + return date.toISOString() + } + return undefined + } + + public decimalToString(number: Decimal | undefined | null): string | undefined { + if (number) { + return number.toString() + } + return undefined + } +} diff --git a/database/logging/CommunityLogging.view.ts b/database/logging/CommunityLogging.view.ts new file mode 100644 index 000000000..1c6d74626 --- /dev/null +++ b/database/logging/CommunityLogging.view.ts @@ -0,0 +1,26 @@ +import { Community } from '../entity/Community' + +import { AbstractLoggingView } from './AbstractLogging.view' + +export class CommunityLoggingView extends AbstractLoggingView { + public constructor(private self: Community) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + id: this.self.id, + foreign: this.self.foreign, + url: this.self.url, + publicKey: this.self.publicKey.toString(this.bufferStringFormat), + communityUuid: this.self.communityUuid, + authenticatedAt: this.dateToString(this.self.authenticatedAt), + name: this.self.name, + description: this.self.description?.substring(0, 24), + creationDate: this.dateToString(this.self.creationDate), + createdAt: this.dateToString(this.self.createdAt), + updatedAt: this.dateToString(this.self.updatedAt), + } + } +} diff --git a/database/logging/ContributionLogging.view.ts b/database/logging/ContributionLogging.view.ts new file mode 100644 index 000000000..c924525d2 --- /dev/null +++ b/database/logging/ContributionLogging.view.ts @@ -0,0 +1,45 @@ +import { Contribution } from '../entity/Contribution' +import { AbstractLoggingView } from './AbstractLogging.view' +import { ContributionMessageLoggingView } from './ContributionMessageLogging.view' +import { TransactionLoggingView } from './TransactionLogging.view' +import { UserLoggingView } from './UserLogging.view' + +export class ContributionLoggingView extends AbstractLoggingView { + public constructor(private self: Contribution) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + id: this.self.id, + user: this.self.user + ? new UserLoggingView(this.self.user).toJSON() + : { id: this.self.userId }, + createdAt: this.dateToString(this.self.createdAt), + resubmissionAt: this.dateToString(this.self.resubmissionAt), + contributionDate: this.dateToString(this.self.contributionDate), + memoLength: this.self.memo.length, + amount: this.decimalToString(this.self.amount), + moderatorId: this.self.moderatorId, + contributionLinkId: this.self.contributionLinkId, + confirmedBy: this.self.confirmedBy, + confirmedAt: this.dateToString(this.self.confirmedAt), + deniedBy: this.self.deniedBy, + deniedAt: this.dateToString(this.self.deniedAt), + contributionType: this.self.contributionType, + contributionStatus: this.self.contributionStatus, + transactionId: this.self.transactionId, + updatedAt: this.dateToString(this.self.updatedAt), + updatedBy: this.self.updatedBy, + deletedAt: this.dateToString(this.self.deletedAt), + deletedBy: this.self.deletedBy, + messages: this.self.messages + ? this.self.messages.map((message) => new ContributionMessageLoggingView(message).toJSON()) + : undefined, + transaction: this.self.transaction + ? new TransactionLoggingView(this.self.transaction).toJSON() + : { id: this.self.transactionId }, + } + } +} diff --git a/database/logging/ContributionMessageLogging.view.ts b/database/logging/ContributionMessageLogging.view.ts new file mode 100644 index 000000000..d05c000bb --- /dev/null +++ b/database/logging/ContributionMessageLogging.view.ts @@ -0,0 +1,30 @@ +import { ContributionMessage } from '../entity/ContributionMessage' +import { AbstractLoggingView } from './AbstractLogging.view' +import { ContributionLoggingView } from './ContributionLogging.view' +import { UserLoggingView } from './UserLogging.view' + +export class ContributionMessageLoggingView extends AbstractLoggingView { + public constructor(private self: ContributionMessage) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + id: this.self.id, + contribution: this.self.contribution + ? new ContributionLoggingView(this.self.contribution).toJSON() + : { id: this.self.contributionId }, + user: this.self.user + ? new UserLoggingView(this.self.user).toJSON() + : { id: this.self.userId }, + messageLength: this.self.message.length, + createdAt: this.dateToString(this.self.createdAt), + updatedAt: this.dateToString(this.self.updatedAt), + deletedAt: this.dateToString(this.self.deletedAt), + deletedBy: this.self.deletedBy, + type: this.self.type, + isModerator: this.self.isModerator, + } + } +} diff --git a/database/logging/DltTransactionLogging.view.ts b/database/logging/DltTransactionLogging.view.ts new file mode 100644 index 000000000..7d1681ce2 --- /dev/null +++ b/database/logging/DltTransactionLogging.view.ts @@ -0,0 +1,23 @@ +import { DltTransaction } from '../entity/DltTransaction' +import { AbstractLoggingView } from './AbstractLogging.view' +import { TransactionLoggingView } from './TransactionLogging.view' + +export class DltTransactionLoggingView extends AbstractLoggingView { + public constructor(private self: DltTransaction) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + id: this.self.id, + transaction: this.self.transaction + ? new TransactionLoggingView(this.self.transaction).toJSON() + : { id: this.self.transactionId }, + messageId: this.self.messageId, + verified: this.self.verified, + createdAt: this.dateToString(this.self.createdAt), + verifiedAt: this.dateToString(this.self.verifiedAt), + } + } +} diff --git a/database/logging/FederatedCommunityLogging.view.ts b/database/logging/FederatedCommunityLogging.view.ts new file mode 100644 index 000000000..4e36cc236 --- /dev/null +++ b/database/logging/FederatedCommunityLogging.view.ts @@ -0,0 +1,24 @@ +import { FederatedCommunity } from '../entity/FederatedCommunity' +import { AbstractLoggingView } from './AbstractLogging.view' + +export class FederatedCommunityLoggingView extends AbstractLoggingView { + public constructor(private self: FederatedCommunity) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + id: this.self.id, + foreign: this.self.foreign, + publicKey: this.self.publicKey.toString(this.bufferStringFormat), + apiVersion: this.self.apiVersion, + endPoint: this.self.endPoint, + lastAnnouncedAt: this.dateToString(this.self.lastAnnouncedAt), + verifiedAt: this.self.verifiedAt, + lastErrorAt: this.self.lastErrorAt, + createdAt: this.dateToString(this.self.createdAt), + updatedAt: this.dateToString(this.self.updatedAt), + } + } +} diff --git a/database/logging/PendingTransactionLogging.view.ts b/database/logging/PendingTransactionLogging.view.ts new file mode 100644 index 000000000..84b7f35b9 --- /dev/null +++ b/database/logging/PendingTransactionLogging.view.ts @@ -0,0 +1,27 @@ +/* eslint-disable no-unused-vars */ +import { PendingTransaction } from '../entity/PendingTransaction' +import { Transaction } from '../entity/Transaction' +import { AbstractLoggingView } from './AbstractLogging.view' +import { TransactionLoggingView } from './TransactionLogging.view' + +// TODO: move enum into database, maybe rename database +enum PendingTransactionState { + NEW = 1, + PENDING = 2, + SETTLED = 3, + REVERTED = 4, +} + +export class PendingTransactionLoggingView extends AbstractLoggingView { + public constructor(private self: PendingTransaction) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + ...new TransactionLoggingView(this.self as Transaction).toJSON(), + state: PendingTransactionState[this.self.state], + } + } +} diff --git a/database/logging/TransactionLogging.view.ts b/database/logging/TransactionLogging.view.ts new file mode 100644 index 000000000..7912c7e5d --- /dev/null +++ b/database/logging/TransactionLogging.view.ts @@ -0,0 +1,56 @@ +/* eslint-disable no-unused-vars */ +import { Transaction } from '../entity/Transaction' +import { AbstractLoggingView } from './AbstractLogging.view' +import { ContributionLoggingView } from './ContributionLogging.view' +import { DltTransactionLoggingView } from './DltTransactionLogging.view' + +// TODO: move enum into database, maybe rename database +enum TransactionTypeId { + CREATION = 1, + SEND = 2, + RECEIVE = 3, + // This is a virtual property, never occurring on the database + DECAY = 4, + LINK_SUMMARY = 5, +} + +export class TransactionLoggingView extends AbstractLoggingView { + public constructor(private self: Transaction) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + id: this.self.id, + previous: this.self.previous, + typeId: TransactionTypeId[this.self.typeId], + transactionLinkId: this.self.transactionLinkId, + amount: this.decimalToString(this.self.amount), + balance: this.decimalToString(this.self.balance), + balanceDate: this.dateToString(this.self.balanceDate), + decay: this.decimalToString(this.self.decay), + decayStart: this.dateToString(this.self.decayStart), + memoLength: this.self.memo.length, + creationDate: this.dateToString(this.self.creationDate), + userId: this.self.userId, + userCommunityUuid: this.self.userCommunityUuid, + userGradidoId: this.self.userGradidoID, + userName: this.self.userName?.substring(0, 3) + '...', + linkedUserId: this.self.linkedUserId, + linkedUserCommunityUuid: this.self.linkedUserCommunityUuid, + linkedUserGradidoID: this.self.linkedUserGradidoID, + linkedUserName: this.self.linkedUserName?.substring(0, 3) + '...', + linkedTransactionId: this.self.linkedTransactionId, + contribution: this.self.contribution + ? new ContributionLoggingView(this.self.contribution) + : undefined, + dltTransaction: this.self.dltTransaction + ? new DltTransactionLoggingView(this.self.dltTransaction).toJSON() + : undefined, + previousTransaction: this.self.previousTransaction + ? new TransactionLoggingView(this.self.previousTransaction).toJSON() + : undefined, + } + } +} diff --git a/database/logging/UserContactLogging.view.ts b/database/logging/UserContactLogging.view.ts new file mode 100644 index 000000000..ebc05843a --- /dev/null +++ b/database/logging/UserContactLogging.view.ts @@ -0,0 +1,35 @@ +/* eslint-disable no-unused-vars */ +import { UserContact } from '../entity/UserContact' +import { AbstractLoggingView } from './AbstractLogging.view' +import { UserLoggingView } from './UserLogging.view' + +enum OptInType { + EMAIL_OPT_IN_REGISTER = 1, + EMAIL_OPT_IN_RESET_PASSWORD = 2, +} + +export class UserContactLoggingView extends AbstractLoggingView { + public constructor(private self: UserContact) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + id: this.self.id, + type: this.self.type, + user: this.self.user + ? new UserLoggingView(this.self.user).toJSON() + : { id: this.self.userId }, + email: this.self.email?.substring(0, 3) + '...', + emailVerificationCode: this.self.emailVerificationCode?.substring(0, 4) + '...', + emailOptInTypeId: OptInType[this.self.emailOptInTypeId], + emailResendCount: this.self.emailResendCount, + emailChecked: this.self.emailChecked, + phone: this.self.phone ? this.self.phone.substring(0, 3) + '...' : undefined, + createdAt: this.dateToString(this.self.createdAt), + updatedAt: this.dateToString(this.self.updatedAt), + deletedAt: this.dateToString(this.self.deletedAt), + } + } +} diff --git a/database/logging/UserLogging.view.ts b/database/logging/UserLogging.view.ts new file mode 100644 index 000000000..19b3ca911 --- /dev/null +++ b/database/logging/UserLogging.view.ts @@ -0,0 +1,60 @@ +/* eslint-disable no-unused-vars */ +import { User } from '../entity/User' +import { AbstractLoggingView } from './AbstractLogging.view' +import { ContributionLoggingView } from './ContributionLogging.view' +import { ContributionMessageLoggingView } from './ContributionMessageLogging.view' +import { UserContactLoggingView } from './UserContactLogging.view' +import { UserRoleLoggingView } from './UserRoleLogging.view' + +enum PasswordEncryptionType { + NO_PASSWORD = 0, + EMAIL = 1, + GRADIDO_ID = 2, +} + +export class UserLoggingView extends AbstractLoggingView { + public constructor(private self: User) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + id: this.self.id, + foreign: this.self.foreign, + gradidoID: this.self.gradidoID, + communityUuid: this.self.communityUuid, + alias: this.self.alias?.substring(0, 3) + '...', + emailContact: this.self.emailContact + ? new UserContactLoggingView(this.self.emailContact).toJSON() + : { id: this.self.emailId }, + firstName: this.self.firstName?.substring(0, 3) + '...', + lastName: this.self.lastName?.substring(0, 3) + '...', + createdAt: this.dateToString(this.self.createdAt), + deletedAt: this.dateToString(this.self.deletedAt), + passwordEncryptionType: this.self.passwordEncryptionType as PasswordEncryptionType, + language: this.self.language, + hideAmountGDD: this.self.hideAmountGDD, + hideAmountGDT: this.self.hideAmountGDT, + userRoles: this.self.userRoles + ? this.self.userRoles.map((userRole) => new UserRoleLoggingView(userRole).toJSON()) + : undefined, + referrerId: this.self.referrerId, + contributionLinkId: this.self.contributionLinkId, + publisherId: this.self.publisherId, + contributions: this.self.contributions + ? this.self.contributions.map((contribution) => + new ContributionLoggingView(contribution).toJSON(), + ) + : undefined, + messages: this.self.messages + ? this.self.messages.map((message) => new ContributionMessageLoggingView(message).toJSON()) + : undefined, + userContacts: this.self.userContacts + ? this.self.userContacts.map((userContact) => + new UserContactLoggingView(userContact).toJSON(), + ) + : undefined, + } + } +} diff --git a/database/logging/UserRoleLogging.view.ts b/database/logging/UserRoleLogging.view.ts new file mode 100644 index 000000000..19050367b --- /dev/null +++ b/database/logging/UserRoleLogging.view.ts @@ -0,0 +1,22 @@ +import { UserRole } from '../entity/UserRole' +import { AbstractLoggingView } from './AbstractLogging.view' +import { UserLoggingView } from './UserLogging.view' + +export class UserRoleLoggingView extends AbstractLoggingView { + public constructor(private self: UserRole) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + id: this.self.id, + user: this.self.user + ? new UserLoggingView(this.self.user).toJSON() + : { id: this.self.userId }, + role: this.self.role, + createdAt: this.dateToString(this.self.createdAt), + updatedAt: this.dateToString(this.self.updatedAt), + } + } +} diff --git a/database/migrations/0079-fill_linked_user_id_of_contributions.ts b/database/migrations/0079-fill_linked_user_id_of_contributions.ts new file mode 100644 index 000000000..5fe62af30 --- /dev/null +++ b/database/migrations/0079-fill_linked_user_id_of_contributions.ts @@ -0,0 +1,13 @@ +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn( + `UPDATE \`transactions\` AS t + JOIN \`contributions\` AS c ON t.id = c.transaction_id + SET t.linked_user_id = c.confirmed_by + WHERE t.type_id = ?`, + [1], + ) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn(`UPDATE \`transactions\` SET \`linked_user_id\` = NULL where \`type_id\` = ?;`, [1]) +} diff --git a/database/migrations/0080-fill_linked_user_gradidoId_of_contributions.ts b/database/migrations/0080-fill_linked_user_gradidoId_of_contributions.ts new file mode 100644 index 000000000..ae5ef6ccf --- /dev/null +++ b/database/migrations/0080-fill_linked_user_gradidoId_of_contributions.ts @@ -0,0 +1,34 @@ +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn( + `UPDATE \`transactions\` AS t + JOIN \`contributions\` AS c ON t.id = c.transaction_id + JOIN \`users\` AS u ON u.id = c.confirmed_by + SET + t.linked_user_gradido_id = u.gradido_id, + t.linked_user_name = CONCAT(u.first_name, ' ', u.last_name), + t.linked_user_community_uuid = u.community_uuid + WHERE t.type_id = ?`, + [1], + ) + + // fill user community uuid fields in transactions + await queryFn( + `UPDATE \`transactions\` AS t + JOIN \`users\` AS u ON u.id = t.user_id + JOIN \`users\` AS lu ON lu.id = t.linked_user_id + SET + t.user_community_uuid = u.community_uuid, + t.linked_user_community_uuid = lu.community_uuid`, + ) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn( + `UPDATE \`transactions\` SET \`linked_user_gradido_id\` = NULL, \`linked_user_name\` = NULL where \`type_id\` = ?;`, + [1], + ) + + await queryFn( + `UPDATE \`transactions\` SET \`user_community_uuid\` = NULL, \`linked_user_community_uuid\` = NULL;`, + ) +} diff --git a/database/migrations/0081-user_join_community.ts b/database/migrations/0081-user_join_community.ts new file mode 100644 index 000000000..6e40cb414 --- /dev/null +++ b/database/migrations/0081-user_join_community.ts @@ -0,0 +1,11 @@ +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn( + 'ALTER TABLE users MODIFY community_uuid VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;', + ) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn( + 'ALTER TABLE users MODIFY community_uuid VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;', + ) +} diff --git a/database/migrations/0079-introduce_gms_registration.ts b/database/migrations/0082-introduce_gms_registration.ts similarity index 100% rename from database/migrations/0079-introduce_gms_registration.ts rename to database/migrations/0082-introduce_gms_registration.ts diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index aa6c6cade..b6b158b87 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -1,87 +1,102 @@ -GRADIDO_LOG_PATH=/home/gradido/gradido/deployment/bare_metal/log +# Need to adjust! +COMMUNITY_NAME="Your community name" +COMMUNITY_DESCRIPTION="Short Description from your Community." +COMMUNITY_HOST=gddhost.tld +COMMUNITY_SUPPORT_MAIL=support@supportmail.com +# setup email account for sending gradido system messages to users +EMAIL=true +EMAIL_USERNAME=peter@lustig.de +EMAIL_SENDER=peter@lustig.de +EMAIL_PASSWORD=1234 +EMAIL_SMTP_URL=smtp.lustig.de +EMAIL_SMTP_PORT=587 + +# if set to true allow sending gradidos to another communities +FEDERATION_XCOM_SENDCOINS_ENABLED=false + +# how many minutes email verification code is valid +# also used for password reset code +EMAIL_CODE_VALID_TIME=1440 +# how many minutes user must wait before he can request the email verification code again +# also used for password reset code +EMAIL_CODE_REQUEST_TIME=10 + +# Need to adjust by updates +# config versions +DATABASE_CONFIG_VERSION=v1.2022-03-18 +BACKEND_CONFIG_VERSION=v21.2024-01-06 +FRONTEND_CONFIG_VERSION=v5.2024-01-08 +ADMIN_CONFIG_VERSION=v2.2024-01-04 +FEDERATION_CONFIG_VERSION=v2.2023-08-24 +FEDERATION_DHT_CONFIG_VERSION=v4.2024-01-17 + +FEDERATION_DHT_TOPIC=GRADIDO_HUB + +# Need adjustments for test system +URL_PROTOCOL=https # start script +# only for test server DEPLOY_SEED_DATA=false +# test email +# if true all email will be send to EMAIL_TEST_RECEIVER instead of email address of user +EMAIL_TEST_MODUS=false +EMAIL_TEST_RECEIVER=test_team@gradido.net -# nginx -NGINX_REWRITE_LEGACY_URLS=true -NGINX_SSL=true -NGINX_SERVER_NAME=stage1.gradido.net -NGINX_SSL_CERTIFICATE=/etc/letsencrypt/live/stage1.gradido.net/fullchain.pem -NGINX_SSL_CERTIFICATE_KEY=/etc/letsencrypt/live/stage1.gradido.net/privkey.pem -NGINX_SSL_DHPARAM=/etc/letsencrypt/ssl-dhparams.pem -NGINX_SSL_INCLUDE=/etc/letsencrypt/options-ssl-nginx.conf -NGINX_UPDATE_PAGE_ROOT=/home/gradido/gradido/deployment/bare_metal/nginx/update-page +# Logging +LOG_LEVEL=INFO +GRADIDO_LOG_PATH=/home/gradido/gradido/deployment/bare_metal/log +TYPEORM_LOGGING_RELATIVE_PATH=../deployment/bare_metal/log/typeorm.backend.log # webhook WEBHOOK_GITHUB_SECRET=secret WEBHOOK_GITHUB_BRANCH=master -# community -COMMUNITY_NAME="Gradido Development Stage1" -COMMUNITY_URL=https://stage1.gradido.net/ -COMMUNITY_REGISTER_URL=https://stage1.gradido.net/register -COMMUNITY_REDEEM_URL=https://stage1.gradido.net/redeem/{code} -COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code} -COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community" -COMMUNITY_SUPPORT_MAIL=support@supportmail.com - -# backend -BACKEND_CONFIG_VERSION=v17.2023-07-03 +# frontend and admin paths, usually don't need changes +# used in nginx config and for links in emails +COMMUNITY_REGISTER_PATH=/register +COMMUNITY_REDEEM_PATH=/redeem/{code} +COMMUNITY_REDEEM_CONTRIBUTION_PATH=/redeem/CL-{code} +WALLET_LOGIN_PATH=/login +WALLET_AUTH_PATH=/authenticate?token={token} +EMAIL_LINK_VERIFICATION_PATH=/checkEmail/{optin}{code} +EMAIL_LINK_SETPASSWORD_PATH=/reset-password/{optin} +EMAIL_LINK_FORGOTPASSWORD_PATH=/forgot-password +EMAIL_LINK_OVERVIEW_PATH=/overview +ADMIN_AUTH_PATH=/admin/authenticate?token={token} +GRAPHQL_PATH=/graphql +# login expire time JWT_EXPIRES_IN=10m + +# Federation +# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen +# on an hash created from this topic +# FEDERATION_DHT_TOPIC=GRADIDO_HUB +# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f +# the api port is the baseport, which will be added with the api-version, e.g. 1_0 = 5010 +FEDERATION_COMMUNITY_API_PORT=5000 +FEDERATION_VALIDATE_COMMUNITY_TIMER=60000 + +# comma separated list of api-versions, which cause starting several federation modules +FEDERATION_COMMUNITY_APIS=1_0 + +# externe gradido services (more added in future) GDT_API_URL=https://gdt.gradido.net -TYPEORM_LOGGING_RELATIVE_PATH=../deployment/bare_metal/log/typeorm.backend.log +# DLT-Connector (still in develop) +DLT_CONNECTOR=false +DLT_CONNECTOR_PORT=6010 +# used for combining a newsletter on klicktipp with this gradido community +# if used, user will be subscribed on register and can unsubscribe in his account KLICKTIPP=false KLICKTIPP_USER= KLICKTIPP_PASSWORD= KLICKTIPP_APIKEY_DE= KLICKTIPP_APIKEY_EN= -EMAIL=true -EMAIL_TEST_MODUS=false -EMAIL_TEST_RECEIVER=test_team@gradido.net -EMAIL_USERNAME=peter@lustig.de -EMAIL_SENDER=peter@lustig.de -EMAIL_PASSWORD=1234 -EMAIL_SMTP_URL=smtp.lustig.de -EMAIL_LINK_VERIFICATION=https://stage1.gradido.net/checkEmail/{optin}{code} -EMAIL_LINK_SETPASSWORD=https://stage1.gradido.net/reset-password/{optin} -EMAIL_LINK_FORGOTPASSWORD=https://stage1.gradido.net/forgot-password -EMAIL_LINK_OVERVIEW=https://stage1.gradido.net/overview -EMAIL_CODE_VALID_TIME=1440 -EMAIL_CODE_REQUEST_TIME=10 - -WEBHOOK_ELOPAGE_SECRET=secret - -# Federation -FEDERATION_DHT_CONFIG_VERSION=v3.2023-04-26 -# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen -# on an hash created from this topic -# FEDERATION_DHT_TOPIC=GRADIDO_HUB -# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f -FEDERATION_COMMUNITY_URL=http://stage1.gradido.net -# the api port is the baseport, which will be added with the api-version, e.g. 1_0 = 5010 -FEDERATION_COMMUNITY_API_PORT=5000 - -FEDERATION_CONFIG_VERSION=v1.2023-01-09 -# comma separated list of api-versions, which cause starting several federation modules -FEDERATION_COMMUNITY_APIS=1_0,1_1 - -# database -DATABASE_CONFIG_VERSION=v1.2022-03-18 - -# frontend -FRONTEND_CONFIG_VERSION=v4.2022-12-20 - -GRAPHQL_URI=https://stage1.gradido.net/graphql -ADMIN_AUTH_URL=https://stage1.gradido.net/admin/authenticate?token={token} - -DEFAULT_PUBLISHER_ID=2896 - -META_URL=http://localhost +# Meta data in frontend pages, important when shared via facebook or twitter or for search engines META_TITLE_DE="Gradido – Dein Dankbarkeitskonto" META_TITLE_EN="Gradido - Your gratitude account" META_DESCRIPTION_DE="Dankbarkeit ist die Währung der neuen Zeit. Immer mehr Menschen entfalten ihr Potenzial und gestalten eine gute Zukunft für alle." @@ -90,8 +105,20 @@ META_KEYWORDS_DE="Grundeinkommen, Währung, Dankbarkeit, Schenk-Ökonomie, Natü META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System" META_AUTHOR="Bernd Hückstädt - Gradido-Akademie" -# admin -ADMIN_CONFIG_VERSION=v1.2022-03-18 +# update page shown while updating gradido +# page will be fed with status changes +NGINX_UPDATE_PAGE_ROOT=/home/gradido/gradido/deployment/bare_metal/nginx/update-page +# NGINX SSL Setup with certbot +# will be generated by start.sh with $COMMUNITY_HOST, only need to setup manual if setup differ from default +#NGINX_SSL_CERTIFICATE=/etc/letsencrypt/live/gddhost.tld/fullchain.pem +#NGINX_SSL_CERTIFICATE_KEY=/etc/letsencrypt/live/gddhost.tld/privkey.pem +NGINX_SSL_DHPARAM=/etc/letsencrypt/ssl-dhparams.pem +NGINX_SSL_INCLUDE=/etc/letsencrypt/options-ssl-nginx.conf + +# LEGACY +NGINX_REWRITE_LEGACY_URLS=false +DEFAULT_PUBLISHER_ID=2896 +WEBHOOK_ELOPAGE_SECRET=secret WALLET_AUTH_URL=https://stage1.gradido.net/authenticate?token={token} WALLET_URL=https://stage1.gradido.net/login diff --git a/deployment/bare_metal/doc/server.drawio b/deployment/bare_metal/doc/server.drawio new file mode 100644 index 000000000..e65220821 --- /dev/null +++ b/deployment/bare_metal/doc/server.drawio @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/deployment/bare_metal/nginx/common/limit_requests.conf b/deployment/bare_metal/nginx/common/limit_requests.conf new file mode 100644 index 000000000..c9501fd64 --- /dev/null +++ b/deployment/bare_metal/nginx/common/limit_requests.conf @@ -0,0 +1,4 @@ +limit_req_zone $binary_remote_addr zone=frontend:20m rate=5r/s; +limit_req_zone $binary_remote_addr zone=backend:25m rate=15r/s; +limit_req_zone $binary_remote_addr zone=api:5m rate=30r/s; +limit_conn_zone $binary_remote_addr zone=addr:10m; \ No newline at end of file diff --git a/deployment/bare_metal/nginx/sites-available/gradido-federation.conf.template b/deployment/bare_metal/nginx/sites-available/gradido-federation.conf.template index 2192b7dbb..5123deb5e 100644 --- a/deployment/bare_metal/nginx/sites-available/gradido-federation.conf.template +++ b/deployment/bare_metal/nginx/sites-available/gradido-federation.conf.template @@ -1,5 +1,8 @@ location /api/$FEDERATION_APIVERSION { + limit_req zone=api burst=60 nodelay; + limit_conn addr 30; + proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; 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 a99327745..d8ed50ba4 100644 --- a/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template +++ b/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template @@ -1,16 +1,18 @@ +include /etc/nginx/common/limit_requests.conf; + server { - if ($host = $NGINX_SERVER_NAME) { + if ($host = $COMMUNITY_HOST) { return 301 https://$host$request_uri; } - server_name $NGINX_SERVER_NAME; + server_name $COMMUNITY_HOST; listen 80; listen [::]:80; return 404; } server { - server_name $NGINX_SERVER_NAME; + server_name $COMMUNITY_HOST; listen [::]:443 ssl ipv6only=on; listen 443 ssl; @@ -22,6 +24,15 @@ server { include /etc/nginx/common/protect.conf; include /etc/nginx/common/protect_add_header.conf; + # protect from slow loris + client_body_timeout 10s; + client_header_timeout 10s; + + # protect from range attack (in http header) + if ($http_range ~ "d{9,}") { + return 444; + } + #gzip_static on; gzip on; gzip_proxied any; @@ -42,6 +53,8 @@ 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'; @@ -58,6 +71,8 @@ server { # Backend location /graphql { + limit_req zone=backend burst=10 nodelay; + limit_conn addr 10; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; @@ -74,6 +89,8 @@ server { # Backend webhooks location /hook { + limit_req zone=backend burst=10; + limit_conn addr 10; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; @@ -90,6 +107,8 @@ server { # Webhook reverse proxy location /hooks/ { + limit_req zone=backend burst=10; + limit_conn addr 10; proxy_pass http://127.0.0.1:9000/hooks/; access_log $GRADIDO_LOG_PATH/nginx-access.hooks.log gradido_log; @@ -98,6 +117,8 @@ server { # Admin Frontend 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'; diff --git a/deployment/bare_metal/nginx/sites-available/gradido.conf.template b/deployment/bare_metal/nginx/sites-available/gradido.conf.template index f6149a818..e0f382467 100644 --- a/deployment/bare_metal/nginx/sites-available/gradido.conf.template +++ b/deployment/bare_metal/nginx/sites-available/gradido.conf.template @@ -1,5 +1,7 @@ +include /etc/nginx/common/limit_requests.conf; + server { - server_name $NGINX_SERVER_NAME; + server_name $COMMUNITY_HOST; listen 80; listen [::]:80; @@ -7,6 +9,15 @@ server { include /etc/nginx/common/protect.conf; include /etc/nginx/common/protect_add_header.conf; + # protect from slow loris + client_body_timeout 10s; + client_header_timeout 10s; + + # protect from range attack (in http header) + if ($http_range ~ "d{9,}") { + return 444; + } + #gzip_static on; gzip on; gzip_proxied any; @@ -27,6 +38,8 @@ 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'; @@ -43,6 +56,8 @@ server { # Backend location /graphql { + limit_req zone=backend burst=10 nodelay; + limit_conn addr 10; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; @@ -59,6 +74,8 @@ server { # Backend webhooks location /hook { + limit_req zone=backend burst=10; + limit_conn addr 10; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; @@ -66,7 +83,6 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; - # no trailing slash to keep the hook/ prefix proxy_pass http://127.0.0.1:4000/hook; proxy_redirect off; @@ -76,6 +92,8 @@ server { # Webhook reverse proxy location /hooks/ { + limit_req zone=backend burst=10; + limit_conn addr 10; proxy_pass http://127.0.0.1:9000/hooks/; access_log $GRADIDO_LOG_PATH/nginx-access.hooks.log gradido_log; @@ -84,6 +102,8 @@ server { # Admin Frontend 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'; @@ -97,7 +117,7 @@ server { access_log $GRADIDO_LOG_PATH/nginx-access.admin.log gradido_log; error_log $GRADIDO_LOG_PATH/nginx-error.admin.log warn; } - + # Federation $FEDERATION_NGINX_CONF diff --git a/deployment/bare_metal/nginx/sites-available/update-page.conf.ssl.template b/deployment/bare_metal/nginx/sites-available/update-page.conf.ssl.template index ddcb9ffc1..fd41c333d 100644 --- a/deployment/bare_metal/nginx/sites-available/update-page.conf.ssl.template +++ b/deployment/bare_metal/nginx/sites-available/update-page.conf.ssl.template @@ -1,16 +1,17 @@ +include /etc/nginx/common/limit_requests.conf; server { - if ($host = $NGINX_SERVER_NAME) { + if ($host = $COMMUNITY_HOST) { return 301 https://$host$request_uri; } - server_name $NGINX_SERVER_NAME; + server_name $COMMUNITY_HOST; listen 80; listen [::]:80; return 404; } server { - server_name $NGINX_SERVER_NAME; + server_name $COMMUNITY_HOST; listen [::]:443 ssl ipv6only=on; listen 443 ssl; @@ -22,12 +23,23 @@ server { include /etc/nginx/common/protect.conf; include /etc/nginx/common/protect_add_header.conf; + # protect from slow loris + client_body_timeout 10s; + client_header_timeout 10s; + + # protect from range attack (in http header) + if ($http_range ~ "d{9,}") { + return 444; + } + gzip on; root $NGINX_UPDATE_PAGE_ROOT; index updating.html; location / { + limit_req zone=frontend; + limit_conn addr 10; try_files /updating.html =404; } diff --git a/deployment/bare_metal/nginx/sites-available/update-page.conf.template b/deployment/bare_metal/nginx/sites-available/update-page.conf.template index c26a705ce..be91abc88 100644 --- a/deployment/bare_metal/nginx/sites-available/update-page.conf.template +++ b/deployment/bare_metal/nginx/sites-available/update-page.conf.template @@ -1,18 +1,30 @@ +include /etc/nginx/common/limit_requests.conf; server { - server_name _; + server_name $COMMUNITY_HOST; listen 80; listen [::]:80; include /etc/nginx/common/protect.conf; include /etc/nginx/common/protect_add_header.conf; + # protect from slow loris + client_body_timeout 10s; + client_header_timeout 10s; + + # protect from range attack (in http header) + if ($http_range ~ "d{9,}") { + return 444; + } + gzip on; root $NGINX_UPDATE_PAGE_ROOT; index updating.html; location / { + limit_req zone=frontend; + limit_conn addr 10; try_files /updating.html =404; } diff --git a/deployment/bare_metal/start.sh b/deployment/bare_metal/start.sh index 5d5744bd6..4b6498ee0 100755 --- a/deployment/bare_metal/start.sh +++ b/deployment/bare_metal/start.sh @@ -10,12 +10,17 @@ PROJECT_ROOT=$SCRIPT_DIR/../.. NGINX_CONFIG_DIR=$SCRIPT_DIR/nginx/sites-available set +o allexport +# enable nvm +export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + # NOTE: all config values will be in process.env when starting # the services and will therefore take precedence over the .env # We have to load the backend .env to get DB_USERNAME, DB_PASSWORD AND JWT_SECRET +# and the dht-node .env to get FEDERATION_DHT_SEED export_var(){ export $1=$(grep -v '^#' $PROJECT_ROOT/backend/.env | grep -e "$1" | sed -e 's/.*=//') + export $1=$(grep -v '^#' $PROJECT_ROOT/dht-node/.env | grep -e "$1" | sed -e 's/.*=//') } if [ -f "$PROJECT_ROOT/backend/.env" ]; then @@ -24,6 +29,10 @@ if [ -f "$PROJECT_ROOT/backend/.env" ]; then export_var 'JWT_SECRET' fi +if [ -f "$PROJECT_ROOT/dht-node/.env" ]; then + export_var 'FEDERATION_DHT_SEED' +fi + # Load .env or .env.dist if not present if [ -f "$SCRIPT_DIR/.env" ]; then set -o allexport @@ -35,6 +44,14 @@ else set +o allexport fi +# set env variables dynamic if not already set in .env or .env.dist +: ${NGINX_SSL_CERTIFICATE:=/etc/letsencrypt/live/$COMMUNITY_HOST/fullchain.pem} +: ${NGINX_SSL_CERTIFICATE_KEY:=/etc/letsencrypt/live/$COMMUNITY_HOST/privkey.pem} + +# export env variables +export NGINX_SSL_CERTIFICATE +export NGINX_SSL_CERTIFICATE_KEY + # lock start if [ -f $LOCK_FILE ] ; then echo "Already building!" @@ -54,8 +71,7 @@ exec > >(tee -a $UPDATE_HTML) 2>&1 # configure nginx for the update-page echo 'Configuring nginx to serve the update-page' >> $UPDATE_HTML -rm /etc/nginx/sites-enabled/gradido.conf -ln -s /etc/nginx/sites-available/update-page.conf /etc/nginx/sites-enabled/ +ln -sf $SCRIPT_DIR/nginx/sites-available/update-page.conf $SCRIPT_DIR/nginx/sites-enabled/default sudo /etc/init.d/nginx restart # stop all services @@ -100,9 +116,9 @@ export FEDERATION_NGINX_CONF=$(< $NGINX_CONFIG_DIR/gradido-federation.conf.locat # *** 3rd generate gradido nginx config including federation modules per api-version echo 'Generate new gradido nginx config' >> $UPDATE_HTML -case "$NGINX_SSL" in - true) TEMPLATE_FILE="gradido.conf.ssl.template" ;; - *) TEMPLATE_FILE="gradido.conf.template" ;; +case "$URL_PROTOCOL" in + 'https') TEMPLATE_FILE="gradido.conf.ssl.template" ;; + *) TEMPLATE_FILE="gradido.conf.template" ;; esac envsubst '$FEDERATION_NGINX_CONF' < $NGINX_CONFIG_DIR/$TEMPLATE_FILE > $NGINX_CONFIG_DIR/gradido.conf.tmp unset FEDERATION_NGINX_CONF @@ -112,9 +128,9 @@ rm $NGINX_CONFIG_DIR/gradido-federation.conf.locations # Generate update-page.conf from template echo 'Generate new update-page nginx config' >> $UPDATE_HTML -case "$NGINX_SSL" in - true) TEMPLATE_FILE="update-page.conf.ssl.template" ;; - *) TEMPLATE_FILE="update-page.conf.template" ;; +case "$URL_PROTOCOL" in + 'https') TEMPLATE_FILE="update-page.conf.ssl.template" ;; + *) TEMPLATE_FILE="update-page.conf.template" ;; esac envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $NGINX_CONFIG_DIR/$TEMPLATE_FILE > $NGINX_CONFIG_DIR/update-page.conf @@ -177,8 +193,7 @@ if [ "$DEPLOY_SEED_DATA" = "true" ]; then fi # TODO maybe handle this differently? export NODE_ENV=production -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 save + # Install & build frontend echo 'Updating frontend' >> $UPDATE_HTML @@ -189,8 +204,6 @@ yarn install yarn build # TODO maybe handle this differently? export NODE_ENV=production -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 save # Install & build admin echo 'Updating admin' >> $UPDATE_HTML @@ -201,8 +214,6 @@ yarn install yarn build # TODO maybe handle this differently? export NODE_ENV=production -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 # Install & build dht-node echo 'Updating dht-node' >> $UPDATE_HTML @@ -213,15 +224,6 @@ yarn install yarn build # TODO maybe handle this differently? export NODE_ENV=production -if [ ! -z $FEDERATION_DHT_TOPIC ]; then - pm2 start --name gradido-dht-node "yarn --cwd $PROJECT_ROOT/dht-node start" -l $GRADIDO_LOG_PATH/pm2.dht-node.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS' - pm2 save -else - echo "=====================================================================" >> $UPDATE_HTML - echo "WARNING: FEDERATION_DHT_TOPIC not configured. DHT-Node not started..." >> $UPDATE_HTML - echo "=====================================================================" >> $UPDATE_HTML -fi - # Install & build federation echo 'Updating federation' >> $UPDATE_HTML @@ -233,6 +235,20 @@ yarn build # TODO maybe handle this differently? 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 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' + pm2 save +else + echo "=====================================================================" >> $UPDATE_HTML + echo "WARNING: FEDERATION_DHT_TOPIC not configured. DHT-Node not started..." >> $UPDATE_HTML + echo "=====================================================================" >> $UPDATE_HTML +fi + # set FEDERATION_PORT from FEDERATION_COMMUNITY_APIS IFS="," read -a API_ARRAY <<< $FEDERATION_COMMUNITY_APIS for api in "${API_ARRAY[@]}" @@ -254,13 +270,9 @@ do pm2 save done - - - # let nginx showing gradido echo 'Configuring nginx to serve gradido again' >> $UPDATE_HTML -ln -s /etc/nginx/sites-available/gradido.conf /etc/nginx/sites-enabled/ -rm /etc/nginx/sites-enabled/update-page.conf +ln -sf $SCRIPT_DIR/nginx/sites-available/gradido.conf $SCRIPT_DIR/nginx/sites-enabled/default sudo /etc/init.d/nginx restart # keep the update log diff --git a/deployment/hetzner_cloud/README.md b/deployment/hetzner_cloud/README.md new file mode 100644 index 000000000..d03ff0b46 --- /dev/null +++ b/deployment/hetzner_cloud/README.md @@ -0,0 +1,124 @@ +# Setup on Hetzner Cloud Server +Suggested minimal Plan: CX41 +4x vCPU, 16 GB Ram, 160 GB Disk Space, 20.71 € per month (04.01.2024) + +Suggested OS: +Debian 12 + +For Hetzner Cloud Server a cloud config can be attached, which will be run before first start +https://community.hetzner.com/tutorials/basic-cloud-config/de +https://cloudinit.readthedocs.io/en/latest/reference/examples.html +You can use our [cloudConfig.yaml](./cloudConfig.yaml) but you must insert you own ssh public key, +like this: +```yaml +ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAkLGbzbG7KIGfkssKJBkc/0EVAzQ/8vjvVHzNdxhK8J yourname +``` + +## After Setup Cloud Server with cloudConfig.yaml +### setup your domain pointing on server ip address +### login to your new server as root +```bash +ssh -i /path/to/privKey root@gddhost.tld +``` + +### Change default shell + +```bash +chsh -s /bin/bash +chsh -s /bin/bash gradido +``` + +### Set password for user `gradido` + +```bash +$ passwd gradido +# enter new password twice +``` + +### Switch to the new user + +```bash +su gradido +``` + +### Test authentication via SSH + +If you logout from the server you can test authentication: + +```bash +$ ssh -i /path/to/privKey gradido@gddhost.tld +# This should log you in and allow you to use sudo commands, which will require the user's password +``` + +### Disable password root login via ssh + +```bash +sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.org +sudo sed -i -e '/^\(#\|\)PermitRootLogin/s/^.*$/PermitRootLogin no/' /etc/ssh/sshd_config +sudo sed -i '$a AllowUsers gradido' /etc/ssh/sshd_config +sudo /etc/init.d/ssh restart +``` + +### Test SSH Access only, no root ssh access + +```bash +$ ssh gradido@gddhost.tld +# Will result in in either a passphrase request for your key or the message 'Permission denied (publickey)' +$ ssh -i /path/to/privKey root@gddhost.tld +# Will result in 'Permission denied (publickey)' +$ ssh -i /path/to/privKey gradido@gddhost.tld +# Will succeed after entering the correct keys passphrase (if any) +``` + +### Install `Gradido` code +```bash +cd ~ +git clone https://github.com/gradido/gradido.git +``` + +### Adjust the values in `.env` + +***!!! Attention !!!*** + +*Don't forget this step! +All your following installations in `install.sh` will fail!* + +*Notes:* + +- *`;` cannot be part of any value!* +- *The GitHub secret is created on GitHub in Settings -> Webhooks.* + +#### Create `.env` and set values + +```bash +cd ~/gradido/deployment/bare_metal +cp .env.dist .env +nano .env + +# adjust values accordingly +``` + +### Run `install.sh` +***!!! Attention !!!*** +Don't use this script if you have custom config in /etc/nginx/conf.d, because this script +will remove it and ln ../bare_metal/nginx/conf.d + +```bash +cd ~/gradido/deployment/hetzner_cloud +sudo ./install.sh +``` + +### Make yourself admin +- Create an account on your new gradido instance +- Click the link in the activation email +- go back to your ssh session and copy this command + +```bash +sudo mysql -D gradido_community -e "insert into user_roles(user_id, role) values((select id from users order by id desc limit 1), 'ADMIN');" +``` + +- it will make last registered user admin +- login with you newly created user +- if you has a link to `Admin Area` it worked and you are admin + diff --git a/deployment/hetzner_cloud/cloudConfig.yaml b/deployment/hetzner_cloud/cloudConfig.yaml new file mode 100644 index 000000000..84658705f --- /dev/null +++ b/deployment/hetzner_cloud/cloudConfig.yaml @@ -0,0 +1,47 @@ +#cloud-config +users: + - name: gradido + groups: users, admin, sudo + sudo: ALL=(ALL) NOPASSWD:/etc/init.d/nginx start,/etc/init.d/nginx stop,/etc/init.d/nginx restart + shell: /bin/bash + ssh_authorized_keys: + - + +packages: + - fail2ban + - python3-systemd + - ufw + - git + - mariadb-server + - nginx + - curl + - build-essential + - gnupg + - certbot + - python3-certbot-nginx + - logrotate + - automysqlbackup + - expect +package_update: true +package_upgrade: true + +runcmd: +- printf "[sshd]\nenabled = true\nbanaction = iptables-multiport" > /etc/fail2ban/jail.local +- systemctl enable fail2ban + +- ufw allow OpenSSH +- ufw allow http +- ufw allow https +- ufw enable + +- sed -i -e '/^\(#\|\)PasswordAuthentication/s/^.*$/PasswordAuthentication no/' /etc/ssh/sshd_config +- sed -i -e '/^\(#\|\)KbdInteractiveAuthentication/s/^.*$/KbdInteractiveAuthentication no/' /etc/ssh/sshd_config +- sed -i -e '/^\(#\|\)ChallengeResponseAuthentication/s/^.*$/ChallengeResponseAuthentication no/' /etc/ssh/sshd_config +- sed -i -e '/^\(#\|\)MaxAuthTries/s/^.*$/MaxAuthTries 3/' /etc/ssh/sshd_config +- sed -i -e '/^\(#\|\)AllowTcpForwarding/s/^.*$/AllowTcpForwarding no/' /etc/ssh/sshd_config +- sed -i -e '/^\(#\|\)X11Forwarding/s/^.*$/X11Forwarding no/' /etc/ssh/sshd_config +- sed -i -e '/^\(#\|\)AllowAgentForwarding/s/^.*$/AllowAgentForwarding no/' /etc/ssh/sshd_config +- sed -i -e '/^\(#\|\)AuthorizedKeysFile/s/^.*$/AuthorizedKeysFile .ssh\/authorized_keys/' /etc/ssh/sshd_config +- sed -i '$a AllowUsers gradido root' /etc/ssh/sshd_config + +- reboot \ No newline at end of file diff --git a/deployment/hetzner_cloud/crontabs.txt b/deployment/hetzner_cloud/crontabs.txt new file mode 100644 index 000000000..c798b58c4 --- /dev/null +++ b/deployment/hetzner_cloud/crontabs.txt @@ -0,0 +1,38 @@ +# Edit this file to introduce tasks to be run by cron. +# +# Each task to run has to be defined through a single line +# indicating with different fields when the task will be run +# and what command to run for the task +# +# To define the time you can provide concrete values for +# minute (m), hour (h), day of month (dom), month (mon), +# and day of week (dow) or use '*' in these fields (for 'any'). +# +# Notice that tasks will be started based on the cron's system +# daemon's notion of time and timezones. +# +# Output of the crontab jobs (including errors) is sent through +# email to the user the crontab file belongs to (unless redirected). +# +# For example, you can run a backup of all your user accounts +# at 5 a.m every week with: +# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/ +# +# For more information see the manual pages of crontab(5) and cron(8) +# +# m h dom mon dow command + +# `yarn` creates output in `/tmp` directory. This output is generated whenever `yarn start` is called. +# This is especially problematic on staging systems where instable versions are automatically deployed which can lead to an ever restarting, +# hence generating a lot of yarn output. +# the following hourly cron clean the /tmp folder +0 * * * * find /tmp -name "yarn--*" -exec rm -r {} \; > /dev/null + +# cronjob for a daily db backup at 3:00am +0 3 * * * ~/gradido/deployment/bare_metal/backup.sh + +# cronjob for a daily logfile clearance at 3:15 +# remove all log files older than 30 days +15 3 * * * ~/gradido/deployment/bare_metal/removeLogFiles.sh + + diff --git a/deployment/hetzner_cloud/install.sh b/deployment/hetzner_cloud/install.sh new file mode 100755 index 000000000..e9ed69e76 --- /dev/null +++ b/deployment/hetzner_cloud/install.sh @@ -0,0 +1,161 @@ +#!/bin/bash + +# Note: This is needed - since there is Summer-Time included in the default server Setup - UTC is REQUIRED for production data +timedatectl set-timezone UTC +timedatectl set-ntp on +apt purge ntp +systemctl start systemd-timesyncd + +set -o allexport +SCRIPT_PATH=$(realpath ../bare_metal) +SCRIPT_DIR=$(dirname $SCRIPT_PATH) +LOCAL_SCRIPT_PATH=$(realpath $0) +LOCAL_SCRIPT_DIR=$(dirname $LOCAL_SCRIPT_PATH) +PROJECT_ROOT=$SCRIPT_DIR/.. +set +o allexport + +# If install.sh will be called more than once +# We have to load the backend .env to get DB_USERNAME, DB_PASSWORD AND JWT_SECRET +# and the dht-node .env to get FEDERATION_DHT_SEED +export_var(){ + export $1=$(grep -v '^#' $PROJECT_ROOT/backend/.env | grep -e "$1" | sed -e 's/.*=//') + export $1=$(grep -v '^#' $PROJECT_ROOT/dht-node/.env | grep -e "$1" | sed -e 's/.*=//') +} + +if [ -f "$PROJECT_ROOT/backend/.env" ]; then + export_var 'DB_USER' + export_var 'DB_PASSWORD' + export_var 'JWT_SECRET' +fi + +if [ -f "$PROJECT_ROOT/dht-node/.env" ]; then + export_var 'FEDERATION_DHT_SEED' +fi + + +# Load .env or .env.dist if not present +# NOTE: all config values will be in process.env when starting +# the services and will therefore take precedence over the .env +if [ -f "$SCRIPT_PATH/.env" ]; then + set -o allexport + source $SCRIPT_PATH/.env + set +o allexport +else + set -o allexport + source $SCRIPT_PATH/.env.dist + set +o allexport +fi + +# Configure git +git config pull.ff only + +# Secure mysql https://gist.github.com/Mins/4602864 +SECURE_MYSQL=$(expect -c " + +set timeout 10 +spawn mysql_secure_installation + +expect \"Enter current password for root (enter for none):\" +send \"\r\" + +expect \"Switch to unix_socket authentication:\" +send \"Y\r\" + +expect \"Change the root password?\" +send \"n\r\" + +expect \"Remove anonymous users?\" +send \"y\r\" + +expect \"Disallow root login remotely?\" +send \"y\r\" + +expect \"Remove test database and access to it?\" +send \"y\r\" + +expect \"Reload privilege tables now?\" +send \"y\r\" + +expect eof +") +echo "$SECURE_MYSQL" + +# Configure fail2ban, seems to not run out of the box on Debian 12 +echo -e "[sshd]\nbackend = systemd" | tee /etc/fail2ban/jail.d/sshd.conf +# enable nginx-limit-req filter to block also user which exceed nginx request limiter +echo -e "[nginx-limit-req]\nenabled = true\nlogpath = $SCRIPT_PATH/log/nginx-error.*.log" | tee /etc/fail2ban/jail.d/nginx-limit-req.conf +# enable nginx bad request filter +echo -e "[nginx-bad-request]\nenabled = true\nlogpath = $SCRIPT_PATH/log/nginx-error.*.log" | tee /etc/fail2ban/jail.d/nginx-bad-request.conf +systemctl restart fail2ban + +# Configure nginx +rm /etc/nginx/sites-enabled/default +envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $SCRIPT_PATH/nginx/sites-available/gradido.conf.template > $SCRIPT_PATH/nginx/sites-available/gradido.conf +envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $SCRIPT_PATH/nginx/sites-available/update-page.conf.template > $SCRIPT_PATH/nginx/sites-available/update-page.conf +mkdir $SCRIPT_PATH/nginx/sites-enabled +ln -s $SCRIPT_PATH/nginx/sites-available/update-page.conf $SCRIPT_PATH/nginx/sites-enabled/default +ln -s $SCRIPT_PATH/nginx/sites-enabled/default /etc/nginx/sites-enabled +ln -s $SCRIPT_PATH/nginx/common /etc/nginx/ +rmdir /etc/nginx/conf.d +ln -s $SCRIPT_PATH/nginx/conf.d /etc/nginx/ + +# setup https with certbot +certbot certonly --nginx --non-interactive --agree-tos --domains $COMMUNITY_HOST --email $COMMUNITY_SUPPORT_MAIL + +# Install node 16. with nvm, with nodesource is depracted +sudo -u gradido bash -c 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash' +# Close and reopen your terminal to start using nvm or run the following to use it now: +sudo -u gradido bash -c 'export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' +sudo -u gradido bash -c '. $HOME/.nvm/nvm.sh && nvm install 16' # first installed version will be set to default automatic + +# Install yarn +sudo -u gradido bash -c '. $HOME/.nvm/nvm.sh && npm i -g yarn' + +# Install pm2 +sudo -u gradido bash -c '. $HOME/.nvm/nvm.sh && npm i -g pm2 && pm2 startup' + +# Install logrotate +envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $SCRIPT_PATH/logrotate/gradido.conf.template > $SCRIPT_PATH/logrotate/gradido.conf +cp $SCRIPT_PATH/logrotate/gradido.conf /etc/logrotate.d/gradido.conf + +# create db user +export DB_USER=gradido +# create a new password only if it not already exist +if [ -z "${DB_PASSWORD}" ]; then + export DB_PASSWORD=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-32};echo); +fi +mysql < $PROJECT_ROOT/database/.env + +# Configure backend +export JWT_SECRET=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-32};echo); +envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/backend/.env.template > $PROJECT_ROOT/backend/.env + +# Configure frontend +envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/frontend/.env.template > $PROJECT_ROOT/frontend/.env + +# Configure admin +envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/admin/.env.template > $PROJECT_ROOT/admin/.env + +# Configure dht-node +export FEDERATION_DHT_SEED=$(< /dev/urandom tr -dc a-f0-9 | head -c 32;echo); +envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/dht-node/.env.template > $PROJECT_ROOT/dht-node/.env + +# Configure federation +envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/federation/.env.template > $PROJECT_ROOT/federation/.env + +# set all created or modified files back to belonging to gradido +chown -R gradido:gradido $PROJECT_ROOT + +# create cronjob to delete yarn output in /tmp and for making backups regulary +sudo -u gradido crontab < $LOCAL_SCRIPT_DIR/crontabs.txt + +# Start gradido +# Note: on first startup some errors will occur - nothing serious +sudo -u gradido $SCRIPT_PATH/start.sh \ No newline at end of file diff --git a/dht-node/.env.dist b/dht-node/.env.dist index 51728d3e1..fa07fc904 100644 --- a/dht-node/.env.dist +++ b/dht-node/.env.dist @@ -15,5 +15,5 @@ TYPEORM_LOGGING_RELATIVE_PATH=typeorm.dht-node.log FEDERATION_DHT_TOPIC=GRADIDO_HUB # FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f FEDERATION_COMMUNITY_URL=http://localhost -# the api port is the dht baseport, which will be added with the supported api-versions, e.g. 1_0 = 5010 -FEDERATION_COMMUNITY_API_PORT=5000 +# comma separated values, which apis should be announced +FEDERATION_COMMUNITY_APIS=1_0 \ No newline at end of file diff --git a/dht-node/.env.template b/dht-node/.env.template index 1278f61be..b4cef7f5e 100644 --- a/dht-node/.env.template +++ b/dht-node/.env.template @@ -1,5 +1,5 @@ # must match the CONFIG_VERSION.EXPECTED definition in scr/config/index.ts -CONFIG_VERSION=v3.2023-04-26 +CONFIG_VERSION=$FEDERATION_DHT_CONFIG_VERSION # Database DB_HOST=localhost @@ -19,5 +19,7 @@ FEDERATION_DHT_CONFIG_VERSION=$FEDERATION_DHT_CONFIG_VERSION # on an hash created from this topic FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED -FEDERATION_COMMUNITY_URL=$FEDERATION_COMMUNITY_URL -FEDERATION_COMMUNITY_API_PORT=$FEDERATION_COMMUNITY_API_PORT +# comma separated values, which apis should be announced +FEDERATION_COMMUNITY_APIS=$FEDERATION_COMMUNITY_APIS +COMMUNITY_HOST=$COMMUNITY_HOST +URL_PROTOCOL=$URL_PROTOCOL diff --git a/dht-node/jest.config.js b/dht-node/jest.config.js index 6540cc68f..0bb09f0e4 100644 --- a/dht-node/jest.config.js +++ b/dht-node/jest.config.js @@ -7,7 +7,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], coverageThreshold: { global: { - lines: 83, + lines: 82, }, }, setupFiles: ['/test/testSetup.ts'], @@ -21,6 +21,11 @@ module.exports = { process.env.NODE_ENV === 'development' ? '/../database/entity/$1' : '/../database/build/entity/$1', + '@logging/(.*)': + // eslint-disable-next-line n/no-process-env + process.env.NODE_ENV === 'development' + ? '/../database/logging/$1' + : '/../database/build/logging/$1', '@dbTools/(.*)': // eslint-disable-next-line n/no-process-env process.env.NODE_ENV === 'development' diff --git a/dht-node/src/config/index.ts b/dht-node/src/config/index.ts index 1cfc7869c..949ae47ce 100644 --- a/dht-node/src/config/index.ts +++ b/dht-node/src/config/index.ts @@ -4,46 +4,50 @@ import dotenv from 'dotenv' dotenv.config() const constants = { - DB_VERSION: '0079-introduce_gms_registration', + DB_VERSION: '0082-introduce_gms_registration', LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info - LOG_LEVEL: process.env.LOG_LEVEL || 'info', + LOG_LEVEL: process.env.LOG_LEVEL ?? 'info', CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v3.2023-04-26', + EXPECTED: 'v4.2024-01-17', CURRENT: '', }, } const server = { - PRODUCTION: process.env.NODE_ENV === 'production' || false, + PRODUCTION: process.env.NODE_ENV === 'production' ?? false, } const database = { - DB_HOST: process.env.DB_HOST || 'localhost', + DB_HOST: process.env.DB_HOST ?? 'localhost', DB_PORT: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306, - DB_USER: process.env.DB_USER || 'root', - DB_PASSWORD: process.env.DB_PASSWORD || '', - DB_DATABASE: process.env.DB_DATABASE || 'gradido_community', + DB_USER: process.env.DB_USER ?? 'root', + DB_PASSWORD: process.env.DB_PASSWORD ?? '', + DB_DATABASE: process.env.DB_DATABASE ?? 'gradido_community', TYPEORM_LOGGING_RELATIVE_PATH: - process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.dht-node.log', + process.env.TYPEORM_LOGGING_RELATIVE_PATH ?? 'typeorm.dht-node.log', } const community = { - COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung', + COMMUNITY_NAME: process.env.COMMUNITY_NAME ?? 'Gradido Entwicklung', COMMUNITY_DESCRIPTION: - process.env.COMMUNITY_DESCRIPTION || 'Gradido-Community einer lokalen Entwicklungsumgebung.', + process.env.COMMUNITY_DESCRIPTION ?? 'Gradido-Community einer lokalen Entwicklungsumgebung.', } +const COMMUNITY_HOST = process.env.COMMUNITY_HOST ?? 'localhost' +const URL_PROTOCOL = process.env.URL_PROTOCOL ?? 'http' +const COMMUNITY_URL = process.env.COMMUNITY_URL ?? `${URL_PROTOCOL}://${COMMUNITY_HOST}` + const federation = { - FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || 'GRADIDO_HUB', - FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null, - FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL || 'http://localhost', - FEDERATION_COMMUNITY_API_PORT: process.env.FEDERATION_COMMUNITY_API_PORT || '5000', + FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC ?? 'GRADIDO_HUB', + FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED ?? null, + FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL ?? COMMUNITY_URL, + FEDERATION_COMMUNITY_APIS: process.env.FEDERATION_COMMUNITY_APIS ?? '1_0', } // Check config version -constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT +constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION ?? constants.CONFIG_VERSION.DEFAULT if ( ![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes( constants.CONFIG_VERSION.CURRENT, diff --git a/dht-node/src/dht_node/ApiVersionType.ts b/dht-node/src/dht_node/ApiVersionType.ts new file mode 100644 index 000000000..84eb3e39b --- /dev/null +++ b/dht-node/src/dht_node/ApiVersionType.ts @@ -0,0 +1,5 @@ +export enum ApiVersionType { + V1_0 = '1_0', + V1_1 = '1_1', // currently no changes + V2_0 = '2_0', // not exist +} diff --git a/dht-node/src/dht_node/index.test.ts b/dht-node/src/dht_node/index.test.ts index 4370a2528..c31869551 100644 --- a/dht-node/src/dht_node/index.test.ts +++ b/dht-node/src/dht_node/index.test.ts @@ -14,6 +14,7 @@ import { CONFIG } from '@/config' import { startDHT } from './index' CONFIG.FEDERATION_DHT_SEED = '64ebcb0e3ad547848fef4197c6e2332f' +CONFIG.FEDERATION_COMMUNITY_APIS = '1_0,1_1,2_0' jest.mock('@hyperswarm/dht') @@ -253,6 +254,29 @@ describe('federation', () => { }) }) + describe('with receiving non ascii character', () => { + beforeEach(() => { + jest.clearAllMocks() + // containing non-ascii character copyright symbol, U+00A9 + socketEventMocks.data(Buffer.from('48656C6C6F2C20C2A92048656C6C6F21', 'hex')) + /* + const buffer = Buffer.from('48656C6C6F2C20C2A92048656C6C6F21', 'hex') + for (const byte of buffer) { + console.log('byte: %o', byte) + if (byte > 127) { + console.log('non ascii char spotted') + } + } + */ + }) + + it('logs the binary data as hex', () => { + expect(logger.warn).toBeCalledWith( + 'received non ascii character, content as hex: 48656c6c6f2c20c2a92048656c6c6f21', + ) + }) + }) + describe('with receiving array of strings', () => { beforeEach(() => { jest.clearAllMocks() diff --git a/dht-node/src/dht_node/index.ts b/dht-node/src/dht_node/index.ts index c9fad8762..fab67e839 100644 --- a/dht-node/src/dht_node/index.ts +++ b/dht-node/src/dht_node/index.ts @@ -3,11 +3,14 @@ import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import DHT from '@hyperswarm/dht' +import { CommunityLoggingView } from '@logging/CommunityLogging.view' import { v4 as uuidv4 } from 'uuid' import { CONFIG } from '@/config' import { logger } from '@/server/logger' +import { ApiVersionType } from './ApiVersionType' + const KEY_SECRET_SEEDBYTES = 32 const POLLTIME = 20000 @@ -15,11 +18,6 @@ const SUCCESSTIME = 120000 const ERRORTIME = 240000 const ANNOUNCETIME = 30000 -enum ApiVersionType { - V1_0 = '1_0', - V1_1 = '1_1', - V2_0 = '2_0', -} type CommunityApi = { api: string url: string @@ -27,6 +25,15 @@ type CommunityApi = { type KeyPair = { publicKey: Buffer; secretKey: Buffer } +function isAscii(buffer: Buffer): boolean { + for (const byte of buffer) { + if (byte > 127) { + return false + } + } + return true +} + export const startDHT = async (topic: string): Promise => { try { const TOPIC = DHT.hash(Buffer.from(topic)) @@ -59,6 +66,10 @@ export const startDHT = async (topic: string): Promise => { ) return } + if (!isAscii(data)) { + logger.warn(`received non ascii character, content as hex: ${data.toString('hex')}`) + return + } logger.info(`data: ${data.toString('ascii')}`) const recApiVersions: CommunityApi[] = JSON.parse(data.toString('ascii')) @@ -190,9 +201,14 @@ export const startDHT = async (topic: string): Promise => { } async function writeFederatedHomeCommunityEntries(pubKey: string): Promise { - const homeApiVersions: CommunityApi[] = Object.values(ApiVersionType).map(function (apiEnum) { + const homeApiVersions: CommunityApi[] = CONFIG.FEDERATION_COMMUNITY_APIS.split(',').map(function ( + api, + ) { + if (!Object.values(ApiVersionType).includes(api as ApiVersionType)) { + throw new Error(`Federation: unknown api version: ${api}`) + } const comApi: CommunityApi = { - api: apiEnum, + api, url: CONFIG.FEDERATION_COMMUNITY_URL + '/api/', } return comApi @@ -227,7 +243,7 @@ async function writeHomeCommunityEntry(keyPair: KeyPair): Promise { homeCom.name = CONFIG.COMMUNITY_NAME homeCom.description = CONFIG.COMMUNITY_DESCRIPTION await DbCommunity.save(homeCom) - logger.info(`home-community updated successfully:`, homeCom) + logger.info(`home-community updated successfully:`, new CommunityLoggingView(homeCom)) } else { // insert a new homecommunity entry including a new ID and a new but ensured unique UUID homeCom = new DbCommunity() @@ -240,7 +256,7 @@ async function writeHomeCommunityEntry(keyPair: KeyPair): Promise { homeCom.description = CONFIG.COMMUNITY_DESCRIPTION homeCom.creationDate = new Date() await DbCommunity.insert(homeCom) - logger.info(`home-community inserted successfully:`, homeCom) + logger.info(`home-community inserted successfully:`, new CommunityLoggingView(homeCom)) } } catch (err) { throw new Error(`Federation: Error writing HomeCommunity-Entry: ${err}`) diff --git a/dht-node/tsconfig.json b/dht-node/tsconfig.json index 2c6104021..33362b054 100644 --- a/dht-node/tsconfig.json +++ b/dht-node/tsconfig.json @@ -52,7 +52,8 @@ /* external */ "@typeorm/*": ["../backend/src/typeorm/*", "../../backend/src/typeorm/*"], "@dbTools/*": ["../database/src/*", "../../database/build/src/*"], - "@entity/*": ["../database/entity/*", "../../database/build/entity/*"] + "@entity/*": ["../database/entity/*", "../../database/build/entity/*"], + "@logging/*": ["../database/logging/*", "../../database/build/logging/*"] }, // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ "typeRoots": ["src/dht_node/@types", "node_modules/@types"], /* List of folders to include type definitions from. */ diff --git a/dlt-connector/.eslintrc.js b/dlt-connector/.eslintrc.js index c477a4cb1..fa43a5f1a 100644 --- a/dlt-connector/.eslintrc.js +++ b/dlt-connector/.eslintrc.js @@ -14,6 +14,7 @@ module.exports = { // 'plugin:import/typescript', // 'plugin:security/recommended', 'plugin:@eslint-community/eslint-comments/recommended', + 'plugin:dci-lint/recommended', ], settings: { 'import/parsers': { @@ -36,6 +37,7 @@ module.exports = { htmlWhitespaceSensitivity: 'ignore', }, ], + // 'dci-lint/literal-role-contracts': 'off' // import // 'import/export': 'error', // 'import/no-deprecated': 'error', @@ -75,30 +77,30 @@ module.exports = { // 'import/no-named-default': 'error', // 'import/no-namespace': 'error', // 'import/no-unassigned-import': 'error', - // 'import/order': [ - // 'error', - // { - // groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], - // 'newlines-between': 'always', - // pathGroups: [ - // { - // pattern: '@?*/**', - // group: 'external', - // position: 'after', - // }, - // { - // pattern: '@/**', - // group: 'external', - // position: 'after', - // }, - // ], - // alphabetize: { - // order: 'asc' /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */, - // caseInsensitive: true /* ignore case. Options: [true, false] */, - // }, - // distinctGroup: true, - // }, - // ], + 'import/order': [ + 'error', + { + groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'], + 'newlines-between': 'always', + pathGroups: [ + { + pattern: '@?*/**', + group: 'external', + position: 'after', + }, + { + pattern: '@/**', + group: 'external', + position: 'after', + }, + ], + alphabetize: { + order: 'asc' /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */, + caseInsensitive: true /* ignore case. Options: [true, false] */, + }, + distinctGroup: true, + }, + ], // 'import/prefer-default-export': 'off', // n 'n/handle-callback-err': 'error', diff --git a/dlt-connector/@types/bip32-ed25519/index.d.ts b/dlt-connector/@types/bip32-ed25519/index.d.ts new file mode 100644 index 000000000..7a3375ab6 --- /dev/null +++ b/dlt-connector/@types/bip32-ed25519/index.d.ts @@ -0,0 +1 @@ +declare module 'bip32-ed25519' diff --git a/dlt-connector/jest.config.js b/dlt-connector/jest.config.js index 723aa840b..69bc64bb2 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: 77, + lines: 66, }, }, setupFiles: ['/test/testSetup.ts'], @@ -17,6 +17,7 @@ module.exports = { '@arg/(.*)': '/src/graphql/arg/$1', '@controller/(.*)': '/src/controller/$1', '@enum/(.*)': '/src/graphql/enum/$1', + '@model/(.*)': '/src/graphql/model/$1', '@resolver/(.*)': '/src/graphql/resolver/$1', '@input/(.*)': '/src/graphql/input/$1', '@proto/(.*)': '/src/proto/$1', diff --git a/dlt-connector/package.json b/dlt-connector/package.json index da26893d1..8b5ae357c 100644 --- a/dlt-connector/package.json +++ b/dlt-connector/package.json @@ -16,10 +16,11 @@ "test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --forceExit --detectOpenHandles" }, "dependencies": { - "@apollo/protobufjs": "^1.2.7", "@apollo/server": "^4.7.5", "@apollo/utils.fetcher": "^3.0.0", "@iota/client": "^2.2.4", + "bip32-ed25519": "^0.0.4", + "bip39": "^3.1.0", "body-parser": "^1.20.2", "class-validator": "^0.14.0", "cors": "^2.8.5", @@ -28,10 +29,13 @@ "dlt-database": "file:../dlt-database", "dotenv": "10.0.0", "express": "4.17.1", + "express-slow-down": "^2.0.1", "graphql": "^16.7.1", "graphql-scalars": "^1.22.2", + "helmet": "^7.1.0", "log4js": "^6.7.1", "nodemon": "^2.0.20", + "protobufjs": "^7.2.5", "reflect-metadata": "^0.1.13", "sodium-native": "^4.0.4", "tsconfig-paths": "^4.1.2", @@ -51,6 +55,7 @@ "eslint-config-prettier": "^8.8.0", "eslint-config-standard": "^17.0.0", "eslint-import-resolver-typescript": "^3.5.4", + "eslint-plugin-dci-lint": "^0.3.0", "eslint-plugin-import": "^2.27.5", "eslint-plugin-jest": "^27.2.1", "eslint-plugin-n": "^15.7.0", diff --git a/dlt-connector/schema.graphql b/dlt-connector/schema.graphql new file mode 100644 index 000000000..4ee07180d --- /dev/null +++ b/dlt-connector/schema.graphql @@ -0,0 +1,98 @@ +# ----------------------------------------------- +# !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!! +# !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! +# ----------------------------------------------- + +type Community { + confirmedAt: String! + createdAt: String! + foreign: Boolean! + id: Int! + iotaTopic: String! + rootPublicKeyHex: String! +} + +input CommunityDraft { + createdAt: String! + foreign: Boolean! + uuid: String! +} + +"""The `Decimal` scalar type to represent currency values""" +scalar Decimal + +"""Type of the transaction""" +enum InputTransactionType { + CREATION + RECEIVE + SEND +} + +type Mutation { + addCommunity(data: CommunityDraft!): TransactionResult! + sendTransaction(data: TransactionDraft!): TransactionResult! +} + +type Query { + communities(confirmed: Boolean, foreign: Boolean, uuid: String): [Community!]! + community(confirmed: Boolean, foreign: Boolean, uuid: String): Community! + isCommunityExist(confirmed: Boolean, foreign: Boolean, uuid: String): Boolean! +} + +input TransactionDraft { + amount: Decimal! + backendTransactionId: Int! + createdAt: String! + recipientUser: UserIdentifier! + senderUser: UserIdentifier! + targetDate: String + type: InputTransactionType! +} + +type TransactionError { + message: String! + name: String! + type: TransactionErrorType! +} + +"""Transaction Error Type""" +enum TransactionErrorType { + ALREADY_EXIST + DB_ERROR + INVALID_SIGNATURE + LOGIC_ERROR + MISSING_PARAMETER + NOT_FOUND + NOT_IMPLEMENTED_YET + PROTO_DECODE_ERROR + PROTO_ENCODE_ERROR +} + +type TransactionRecipe { + createdAt: String! + id: Int! + topic: String! + type: TransactionType! +} + +type TransactionResult { + error: TransactionError + recipe: TransactionRecipe + succeed: Boolean! +} + +"""Type of the transaction""" +enum TransactionType { + COMMUNITY_ROOT + GRADIDO_CREATION + GRADIDO_DEFERRED_TRANSFER + GRADIDO_TRANSFER + GROUP_FRIENDS_UPDATE + REGISTER_ADDRESS +} + +input UserIdentifier { + accountNr: Int = 1 + communityUuid: String + uuid: String! +} \ No newline at end of file diff --git a/dlt-connector/src/config/index.ts b/dlt-connector/src/config/index.ts index fc8c780b8..e6febb482 100644 --- a/dlt-connector/src/config/index.ts +++ b/dlt-connector/src/config/index.ts @@ -4,9 +4,9 @@ dotenv.config() const constants = { LOG4JS_CONFIG: 'log4js-config.json', - DB_VERSION: '0002-refactor_add_community', + DB_VERSION: '0003-refactor_transaction_recipe', // default log level on production should be info - LOG_LEVEL: process.env.LOG_LEVEL || 'info', + LOG_LEVEL: process.env.LOG_LEVEL ?? 'info', CONFIG_VERSION: { DEFAULT: 'DEFAULT', EXPECTED: 'v4.2023-09-12', @@ -15,7 +15,7 @@ const constants = { } const server = { - PRODUCTION: process.env.NODE_ENV === 'production' || false, + PRODUCTION: process.env.NODE_ENV === 'production' ?? false, } const database = { @@ -35,11 +35,11 @@ const iota = { } const dltConnector = { - DLT_CONNECTOR_PORT: process.env.DLT_CONNECTOR_PORT || 6010, + DLT_CONNECTOR_PORT: process.env.DLT_CONNECTOR_PORT ?? 6010, } // Check config version -constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT +constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION ?? constants.CONFIG_VERSION.DEFAULT if ( ![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes( constants.CONFIG_VERSION.CURRENT, diff --git a/dlt-connector/src/controller/Community.test.ts b/dlt-connector/src/controller/Community.test.ts deleted file mode 100644 index d8c5ad0de..000000000 --- a/dlt-connector/src/controller/Community.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import 'reflect-metadata' -import { CommunityDraft } from '@/graphql/input/CommunityDraft' -import { create as createCommunity, getAllTopics, isExist } from './Community' -import { TestDB } from '@test/TestDB' -import { getDataSource } from '@/typeorm/DataSource' -import { Community } from '@entity/Community' -import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter' - -jest.mock('@typeorm/DataSource', () => ({ - getDataSource: () => TestDB.instance.dbConnect, -})) - -describe('controller/Community', () => { - beforeAll(async () => { - await TestDB.instance.setupTestDB() - // apolloTestServer = await createApolloTestServer() - }) - - afterAll(async () => { - await TestDB.instance.teardownTestDB() - }) - - describe('createCommunity', () => { - it('valid community', async () => { - const communityDraft = new CommunityDraft() - communityDraft.foreign = false - communityDraft.createdAt = '2022-05-01T17:00:12.128Z' - communityDraft.uuid = '3d813cbb-47fb-32ba-91df-831e1593ac29' - - const iotaTopic = iotaTopicFromCommunityUUID(communityDraft.uuid) - expect(iotaTopic).toEqual('204ef6aed15fbf0f9da5819e88f8eea8e3adbe1e2c2d43280780a4b8c2d32b56') - - const createdAtDate = new Date(communityDraft.createdAt) - const communityEntity = createCommunity(communityDraft) - expect(communityEntity).toMatchObject({ - iotaTopic, - createdAt: createdAtDate, - foreign: false, - }) - await getDataSource().manager.save(communityEntity) - }) - }) - - describe('list communities', () => { - it('get all topics', async () => { - expect(await getAllTopics()).toMatchObject([ - '204ef6aed15fbf0f9da5819e88f8eea8e3adbe1e2c2d43280780a4b8c2d32b56', - ]) - }) - - it('isExist with communityDraft', async () => { - const communityDraft = new CommunityDraft() - communityDraft.foreign = false - communityDraft.createdAt = '2022-05-01T17:00:12.128Z' - communityDraft.uuid = '3d813cbb-47fb-32ba-91df-831e1593ac29' - expect(await isExist(communityDraft)).toBe(true) - }) - - it('createdAt with ms precision', async () => { - const list = await Community.findOne({ where: { foreign: false } }) - expect(list).toMatchObject({ - createdAt: new Date('2022-05-01T17:00:12.128Z'), - }) - }) - }) -}) diff --git a/dlt-connector/src/controller/Community.ts b/dlt-connector/src/controller/Community.ts deleted file mode 100644 index eff1b2b64..000000000 --- a/dlt-connector/src/controller/Community.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { CommunityDraft } from '@/graphql/input/CommunityDraft' -import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter' -import { Community } from '@entity/Community' - -export const isExist = async (community: CommunityDraft | string): Promise => { - const iotaTopic = - community instanceof CommunityDraft ? iotaTopicFromCommunityUUID(community.uuid) : community - const result = await Community.find({ - where: { iotaTopic }, - }) - return result.length > 0 -} - -export const create = (community: CommunityDraft, topic?: string): Community => { - const communityEntity = Community.create() - communityEntity.iotaTopic = topic ?? iotaTopicFromCommunityUUID(community.uuid) - communityEntity.createdAt = new Date(community.createdAt) - communityEntity.foreign = community.foreign - if (!community.foreign) { - // TODO: generate keys - } - return communityEntity -} - -export const getAllTopics = async (): Promise => { - const communities = await Community.find({ select: { iotaTopic: true } }) - return communities.map((community) => community.iotaTopic) -} diff --git a/dlt-connector/src/controller/GradidoTransaction.ts b/dlt-connector/src/controller/GradidoTransaction.ts deleted file mode 100644 index 671f3f57a..000000000 --- a/dlt-connector/src/controller/GradidoTransaction.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { GradidoTransaction } from '@/proto/3_3/GradidoTransaction' -import { TransactionBody } from '@/proto/3_3/TransactionBody' - -export const create = (body: TransactionBody): GradidoTransaction => { - const transaction = new GradidoTransaction({ - bodyBytes: Buffer.from(TransactionBody.encode(body).finish()), - }) - // TODO: add correct signature(s) - return transaction -} diff --git a/dlt-connector/src/controller/TransactionBase.ts b/dlt-connector/src/controller/TransactionBase.ts deleted file mode 100644 index 9833226a9..000000000 --- a/dlt-connector/src/controller/TransactionBase.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { TransactionValidationLevel } from '@/graphql/enum/TransactionValidationLevel' - -export abstract class TransactionBase { - // validate if transaction is valid, maybe expensive because depending on level several transactions will be fetched from db - public abstract validate(level: TransactionValidationLevel): boolean -} diff --git a/dlt-connector/src/controller/TransactionBody.test.ts b/dlt-connector/src/controller/TransactionBody.test.ts deleted file mode 100644 index eac613ab7..000000000 --- a/dlt-connector/src/controller/TransactionBody.test.ts +++ /dev/null @@ -1,162 +0,0 @@ -import 'reflect-metadata' -import { TransactionDraft } from '@/graphql/input/TransactionDraft' -import { create, determineCrossGroupType, determineOtherGroup } from './TransactionBody' -import { UserIdentifier } from '@/graphql/input/UserIdentifier' -import { TransactionError } from '@/graphql/model/TransactionError' -import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' -import { CrossGroupType } from '@/graphql/enum/CrossGroupType' -import { TransactionType } from '@/graphql/enum/TransactionType' -import Decimal from 'decimal.js-light' - -describe('test controller/TransactionBody', () => { - describe('test create ', () => { - const senderUser = new UserIdentifier() - const recipientUser = new UserIdentifier() - it('test with contribution transaction', () => { - const transactionDraft = new TransactionDraft() - transactionDraft.senderUser = senderUser - transactionDraft.recipientUser = recipientUser - transactionDraft.type = TransactionType.CREATION - transactionDraft.amount = new Decimal(1000) - transactionDraft.createdAt = '2022-01-02T19:10:34.121' - transactionDraft.targetDate = '2021-12-01T10:05:00.191' - const body = create(transactionDraft) - - expect(body.creation).toBeDefined() - expect(body).toMatchObject({ - createdAt: { - seconds: 1641150634, - nanoSeconds: 121000000, - }, - versionNumber: '3.3', - type: CrossGroupType.LOCAL, - otherGroup: '', - creation: { - recipient: { - amount: '1000', - }, - targetDate: { - seconds: 1638353100, - }, - }, - }) - }) - it('test with local send transaction send part', () => { - const transactionDraft = new TransactionDraft() - transactionDraft.senderUser = senderUser - transactionDraft.recipientUser = recipientUser - transactionDraft.type = TransactionType.SEND - transactionDraft.amount = new Decimal(1000) - transactionDraft.createdAt = '2022-01-02T19:10:34.121' - const body = create(transactionDraft) - - expect(body.transfer).toBeDefined() - expect(body).toMatchObject({ - createdAt: { - seconds: 1641150634, - nanoSeconds: 121000000, - }, - versionNumber: '3.3', - type: CrossGroupType.LOCAL, - otherGroup: '', - transfer: { - sender: { - amount: '1000', - }, - }, - }) - }) - - it('test with local send transaction receive part', () => { - const transactionDraft = new TransactionDraft() - transactionDraft.senderUser = senderUser - transactionDraft.recipientUser = recipientUser - transactionDraft.type = TransactionType.RECEIVE - transactionDraft.amount = new Decimal(1000) - transactionDraft.createdAt = '2022-01-02T19:10:34.121' - const body = create(transactionDraft) - - expect(body.transfer).toBeDefined() - expect(body).toMatchObject({ - createdAt: { - seconds: 1641150634, - nanoSeconds: 121000000, - }, - versionNumber: '3.3', - type: CrossGroupType.LOCAL, - otherGroup: '', - transfer: { - sender: { - amount: '1000', - }, - }, - }) - }) - }) - describe('test determineCrossGroupType', () => { - const transactionDraft = new TransactionDraft() - transactionDraft.senderUser = new UserIdentifier() - transactionDraft.recipientUser = new UserIdentifier() - - it('local transaction', () => { - expect(determineCrossGroupType(transactionDraft)).toEqual(CrossGroupType.LOCAL) - }) - - it('test with with invalid input', () => { - transactionDraft.recipientUser.communityUuid = 'a72a4a4a-aa12-4f6c-b3d8-7cc65c67e24a' - expect(() => determineCrossGroupType(transactionDraft)).toThrow( - new TransactionError( - TransactionErrorType.NOT_IMPLEMENTED_YET, - 'cannot determine CrossGroupType', - ), - ) - }) - - it('inbound transaction (send to sender community)', () => { - transactionDraft.type = TransactionType.SEND - expect(determineCrossGroupType(transactionDraft)).toEqual(CrossGroupType.INBOUND) - }) - - it('outbound transaction (send to recipient community)', () => { - transactionDraft.type = TransactionType.RECEIVE - expect(determineCrossGroupType(transactionDraft)).toEqual(CrossGroupType.OUTBOUND) - }) - }) - - describe('test determineOtherGroup', () => { - const transactionDraft = new TransactionDraft() - transactionDraft.senderUser = new UserIdentifier() - transactionDraft.recipientUser = new UserIdentifier() - - it('for inbound transaction, other group is from recipient, missing community id for recipient', () => { - expect(() => determineOtherGroup(CrossGroupType.INBOUND, transactionDraft)).toThrowError( - new TransactionError( - TransactionErrorType.MISSING_PARAMETER, - 'missing recipient user community id for cross group transaction', - ), - ) - }) - it('for inbound transaction, other group is from recipient', () => { - transactionDraft.recipientUser.communityUuid = 'b8e9f00a-5a56-4b23-8c44-6823ac9e0d2d' - expect(determineOtherGroup(CrossGroupType.INBOUND, transactionDraft)).toEqual( - 'b8e9f00a-5a56-4b23-8c44-6823ac9e0d2d', - ) - }) - - it('for outbound transaction, other group is from sender, missing community id for sender', () => { - expect(() => determineOtherGroup(CrossGroupType.OUTBOUND, transactionDraft)).toThrowError( - new TransactionError( - TransactionErrorType.MISSING_PARAMETER, - 'missing sender user community id for cross group transaction', - ), - ) - }) - - it('for outbound transaction, other group is from sender', () => { - transactionDraft.senderUser.communityUuid = 'a72a4a4a-aa12-4f6c-b3d8-7cc65c67e24a' - expect(determineOtherGroup(CrossGroupType.OUTBOUND, transactionDraft)).toEqual( - 'a72a4a4a-aa12-4f6c-b3d8-7cc65c67e24a', - ) - }) - }) -}) diff --git a/dlt-connector/src/controller/TransactionBody.ts b/dlt-connector/src/controller/TransactionBody.ts deleted file mode 100644 index ae5f37710..000000000 --- a/dlt-connector/src/controller/TransactionBody.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { CrossGroupType } from '@/graphql/enum/CrossGroupType' -import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' -import { TransactionType } from '@/graphql/enum/TransactionType' -import { TransactionDraft } from '@/graphql/input/TransactionDraft' -import { TransactionError } from '@/graphql/model/TransactionError' -import { GradidoCreation } from '@/proto/3_3/GradidoCreation' -import { GradidoTransfer } from '@/proto/3_3/GradidoTransfer' -import { TransactionBody } from '@/proto/3_3/TransactionBody' - -export const create = (transaction: TransactionDraft): TransactionBody => { - const body = new TransactionBody(transaction) - // TODO: load pubkeys for sender and recipient user from db - switch (transaction.type) { - case TransactionType.CREATION: - body.creation = new GradidoCreation(transaction) - body.data = 'gradidoCreation' - break - case TransactionType.SEND: - case TransactionType.RECEIVE: - body.transfer = new GradidoTransfer(transaction) - body.data = 'gradidoTransfer' - break - } - return body -} - -export const determineCrossGroupType = ({ - senderUser, - recipientUser, - type, -}: TransactionDraft): CrossGroupType => { - if ( - !recipientUser.communityUuid || - recipientUser.communityUuid === '' || - senderUser.communityUuid === recipientUser.communityUuid || - type === TransactionType.CREATION - ) { - return CrossGroupType.LOCAL - } else if (type === TransactionType.SEND) { - return CrossGroupType.INBOUND - } else if (type === TransactionType.RECEIVE) { - return CrossGroupType.OUTBOUND - } - throw new TransactionError( - TransactionErrorType.NOT_IMPLEMENTED_YET, - 'cannot determine CrossGroupType', - ) -} - -export const determineOtherGroup = ( - type: CrossGroupType, - { senderUser, recipientUser }: TransactionDraft, -): string => { - switch (type) { - case CrossGroupType.LOCAL: - return '' - case CrossGroupType.INBOUND: - if (!recipientUser.communityUuid) { - throw new TransactionError( - TransactionErrorType.MISSING_PARAMETER, - 'missing recipient user community id for cross group transaction', - ) - } - return recipientUser.communityUuid - case CrossGroupType.OUTBOUND: - if (!senderUser.communityUuid) { - throw new TransactionError( - TransactionErrorType.MISSING_PARAMETER, - 'missing sender user community id for cross group transaction', - ) - } - return senderUser.communityUuid - } -} diff --git a/dlt-connector/src/data/Account.factory.ts b/dlt-connector/src/data/Account.factory.ts new file mode 100644 index 000000000..a8c1f162d --- /dev/null +++ b/dlt-connector/src/data/Account.factory.ts @@ -0,0 +1,60 @@ +import { Account } from '@entity/Account' +import Decimal from 'decimal.js-light' + +import { KeyPair } from '@/data/KeyPair' +import { AddressType } from '@/data/proto/3_3/enum/AddressType' +import { UserAccountDraft } from '@/graphql/input/UserAccountDraft' +import { hardenDerivationIndex } from '@/utils/derivationHelper' +import { accountTypeToAddressType } from '@/utils/typeConverter' + +const GMW_ACCOUNT_DERIVATION_INDEX = 1 +const AUF_ACCOUNT_DERIVATION_INDEX = 2 + +export class AccountFactory { + public static createAccount( + createdAt: Date, + derivationIndex: number, + type: AddressType, + parentKeyPair: KeyPair, + ): Account { + const account = Account.create() + account.derivationIndex = derivationIndex + account.derive2Pubkey = parentKeyPair.derive([derivationIndex]).publicKey + account.type = type.valueOf() + account.createdAt = createdAt + account.balanceOnConfirmation = new Decimal(0) + account.balanceOnCreation = new Decimal(0) + account.balanceCreatedAt = createdAt + return account + } + + public static createAccountFromUserAccountDraft( + { createdAt, accountType, user }: UserAccountDraft, + parentKeyPair: KeyPair, + ): Account { + return AccountFactory.createAccount( + new Date(createdAt), + user.accountNr ?? 1, + accountTypeToAddressType(accountType), + parentKeyPair, + ) + } + + public static createGmwAccount(keyPair: KeyPair, createdAt: Date): Account { + return AccountFactory.createAccount( + createdAt, + hardenDerivationIndex(GMW_ACCOUNT_DERIVATION_INDEX), + AddressType.COMMUNITY_GMW, + keyPair, + ) + } + + public static createAufAccount(keyPair: KeyPair, createdAt: Date): Account { + return AccountFactory.createAccount( + createdAt, + hardenDerivationIndex(AUF_ACCOUNT_DERIVATION_INDEX), + AddressType.COMMUNITY_AUF, + keyPair, + ) + } +} diff --git a/dlt-connector/src/data/Account.repository.ts b/dlt-connector/src/data/Account.repository.ts new file mode 100644 index 000000000..6931e6ea6 --- /dev/null +++ b/dlt-connector/src/data/Account.repository.ts @@ -0,0 +1,34 @@ +import { Account } from '@entity/Account' +import { User } from '@entity/User' +import { In } from 'typeorm' + +import { UserIdentifier } from '@/graphql/input/UserIdentifier' +import { getDataSource } from '@/typeorm/DataSource' + +export const AccountRepository = getDataSource() + .getRepository(Account) + .extend({ + findAccountsByPublicKeys(publicKeys: Buffer[]): Promise { + return this.findBy({ derive2Pubkey: In(publicKeys) }) + }, + + async findAccountByPublicKey(publicKey: Buffer | undefined): Promise { + if (!publicKey) return undefined + return (await this.findOneBy({ derive2Pubkey: Buffer.from(publicKey) })) ?? undefined + }, + + async findAccountByUserIdentifier({ + uuid, + accountNr, + }: UserIdentifier): Promise { + const user = await User.findOne({ + where: { gradidoID: uuid, accounts: { derivationIndex: accountNr ?? 1 } }, + relations: { accounts: true }, + }) + if (user && user.accounts?.length === 1) { + const account = user.accounts[0] + account.user = user + return account + } + }, + }) diff --git a/dlt-connector/src/data/Account.test.ts b/dlt-connector/src/data/Account.test.ts new file mode 100644 index 000000000..f28065cce --- /dev/null +++ b/dlt-connector/src/data/Account.test.ts @@ -0,0 +1,197 @@ +import 'reflect-metadata' +import { Decimal } from 'decimal.js-light' + +import { TestDB } from '@test/TestDB' + +import { AccountType } from '@/graphql/enum/AccountType' +import { UserAccountDraft } from '@/graphql/input/UserAccountDraft' +import { UserIdentifier } from '@/graphql/input/UserIdentifier' + +import { AccountFactory } from './Account.factory' +import { AccountRepository } from './Account.repository' +import { KeyPair } from './KeyPair' +import { Mnemonic } from './Mnemonic' +import { AddressType } from './proto/3_3/enum/AddressType' +import { UserFactory } from './User.factory' +import { UserLogic } from './User.logic' + +const con = TestDB.instance + +jest.mock('@typeorm/DataSource', () => ({ + getDataSource: jest.fn(() => TestDB.instance.dbConnect), +})) + +describe('data/Account test factory and repository', () => { + const now = new Date() + const keyPair1 = new KeyPair(new Mnemonic('62ef251edc2416f162cd24ab1711982b')) + const keyPair2 = new KeyPair(new Mnemonic('000a0000000002000000000003000070')) + const keyPair3 = new KeyPair(new Mnemonic('00ba541a1000020000000000300bda70')) + const userGradidoID = '6be949ab-8198-4acf-ba63-740089081d61' + + describe('test factory methods', () => { + beforeAll(async () => { + await con.setupTestDB() + }) + afterAll(async () => { + await con.teardownTestDB() + }) + + it('test createAccount', () => { + const account = AccountFactory.createAccount(now, 1, AddressType.COMMUNITY_HUMAN, keyPair1) + expect(account).toMatchObject({ + derivationIndex: 1, + derive2Pubkey: Buffer.from( + 'cb88043ef4833afc01d6ed9b34e1aa48e79dce5ff97c07090c6600ec05f6d994', + 'hex', + ), + type: AddressType.COMMUNITY_HUMAN, + createdAt: now, + balanceCreatedAt: now, + balanceOnConfirmation: new Decimal(0), + balanceOnCreation: new Decimal(0), + }) + }) + + it('test createAccountFromUserAccountDraft', () => { + const userAccountDraft = new UserAccountDraft() + userAccountDraft.createdAt = now.toISOString() + userAccountDraft.accountType = AccountType.COMMUNITY_HUMAN + userAccountDraft.user = new UserIdentifier() + userAccountDraft.user.accountNr = 1 + const account = AccountFactory.createAccountFromUserAccountDraft(userAccountDraft, keyPair1) + expect(account).toMatchObject({ + derivationIndex: 1, + derive2Pubkey: Buffer.from( + 'cb88043ef4833afc01d6ed9b34e1aa48e79dce5ff97c07090c6600ec05f6d994', + 'hex', + ), + type: AddressType.COMMUNITY_HUMAN, + createdAt: now, + balanceCreatedAt: now, + balanceOnConfirmation: new Decimal(0), + balanceOnCreation: new Decimal(0), + }) + }) + + it('test createGmwAccount', () => { + const account = AccountFactory.createGmwAccount(keyPair1, now) + expect(account).toMatchObject({ + derivationIndex: 2147483649, + derive2Pubkey: Buffer.from( + '05f0060357bb73bd290283870fc47a10b3764f02ca26938479ed853f46145366', + 'hex', + ), + type: AddressType.COMMUNITY_GMW, + createdAt: now, + balanceCreatedAt: now, + balanceOnConfirmation: new Decimal(0), + balanceOnCreation: new Decimal(0), + }) + }) + + it('test createAufAccount', () => { + const account = AccountFactory.createAufAccount(keyPair1, now) + expect(account).toMatchObject({ + derivationIndex: 2147483650, + derive2Pubkey: Buffer.from( + '6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59', + 'hex', + ), + type: AddressType.COMMUNITY_AUF, + createdAt: now, + balanceCreatedAt: now, + balanceOnConfirmation: new Decimal(0), + balanceOnCreation: new Decimal(0), + }) + }) + }) + + describe('test repository functions', () => { + beforeAll(async () => { + await con.setupTestDB() + await Promise.all([ + AccountFactory.createAufAccount(keyPair1, now).save(), + AccountFactory.createGmwAccount(keyPair1, now).save(), + AccountFactory.createAufAccount(keyPair2, now).save(), + AccountFactory.createGmwAccount(keyPair2, now).save(), + AccountFactory.createAufAccount(keyPair3, now).save(), + AccountFactory.createGmwAccount(keyPair3, now).save(), + ]) + const userAccountDraft = new UserAccountDraft() + userAccountDraft.accountType = AccountType.COMMUNITY_HUMAN + userAccountDraft.createdAt = now.toString() + userAccountDraft.user = new UserIdentifier() + userAccountDraft.user.accountNr = 1 + userAccountDraft.user.uuid = userGradidoID + const user = UserFactory.create(userAccountDraft, keyPair1) + const userLogic = new UserLogic(user) + const account = AccountFactory.createAccountFromUserAccountDraft( + userAccountDraft, + userLogic.calculateKeyPair(keyPair1), + ) + account.user = user + // user is set to cascade: ['insert'] will be saved together with account + await account.save() + }) + afterAll(async () => { + await con.teardownTestDB() + }) + it('test findAccountsByPublicKeys', async () => { + const accounts = await AccountRepository.findAccountsByPublicKeys([ + Buffer.from('6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59', 'hex'), + Buffer.from('0fa996b73b624592fe326b8500cb1e3f10026112b374d84c87d097f4d489c019', 'hex'), + Buffer.from('0ffa996b73b624592f26b850b0cb1e3f1026112b374d84c87d017f4d489c0197', 'hex'), // invalid + ]) + expect(accounts).toHaveLength(2) + expect(accounts).toMatchObject( + expect.arrayContaining([ + expect.objectContaining({ + derivationIndex: 2147483649, + derive2Pubkey: Buffer.from( + '0fa996b73b624592fe326b8500cb1e3f10026112b374d84c87d097f4d489c019', + 'hex', + ), + type: AddressType.COMMUNITY_GMW, + }), + expect.objectContaining({ + derivationIndex: 2147483650, + derive2Pubkey: Buffer.from( + '6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59', + 'hex', + ), + type: AddressType.COMMUNITY_AUF, + }), + ]), + ) + }) + + it('test findAccountByPublicKey', async () => { + expect( + await AccountRepository.findAccountByPublicKey( + Buffer.from('6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59', 'hex'), + ), + ).toMatchObject({ + derivationIndex: 2147483650, + derive2Pubkey: Buffer.from( + '6c749f8693a4a58c948e5ae54df11e2db33d2f98673b56e0cf19c0132614ab59', + 'hex', + ), + type: AddressType.COMMUNITY_AUF, + }) + }) + + it('test findAccountByUserIdentifier', async () => { + const userIdentifier = new UserIdentifier() + userIdentifier.accountNr = 1 + userIdentifier.uuid = userGradidoID + expect(await AccountRepository.findAccountByUserIdentifier(userIdentifier)).toMatchObject({ + derivationIndex: 1, + derive2Pubkey: Buffer.from( + '2099c004a26e5387c9fbbc9bb0f552a9642d3fd7c710ae5802b775d24ff36f93', + 'hex', + ), + type: AddressType.COMMUNITY_HUMAN, + }) + }) + }) +}) diff --git a/dlt-connector/src/data/BackendTransaction.factory.ts b/dlt-connector/src/data/BackendTransaction.factory.ts new file mode 100644 index 000000000..365da0693 --- /dev/null +++ b/dlt-connector/src/data/BackendTransaction.factory.ts @@ -0,0 +1,13 @@ +import { BackendTransaction } from '@entity/BackendTransaction' + +import { TransactionDraft } from '@/graphql/input/TransactionDraft' + +export class BackendTransactionFactory { + public static createFromTransactionDraft(transactionDraft: TransactionDraft): BackendTransaction { + const backendTransaction = BackendTransaction.create() + backendTransaction.backendTransactionId = transactionDraft.backendTransactionId + backendTransaction.typeId = transactionDraft.type + backendTransaction.createdAt = new Date(transactionDraft.createdAt) + return backendTransaction + } +} diff --git a/dlt-connector/src/data/BackendTransaction.repository.ts b/dlt-connector/src/data/BackendTransaction.repository.ts new file mode 100644 index 000000000..b4e566659 --- /dev/null +++ b/dlt-connector/src/data/BackendTransaction.repository.ts @@ -0,0 +1,7 @@ +import { BackendTransaction } from '@entity/BackendTransaction' + +import { getDataSource } from '@/typeorm/DataSource' + +export const BackendTransactionRepository = getDataSource() + .getRepository(BackendTransaction) + .extend({}) diff --git a/dlt-connector/src/data/Community.repository.ts b/dlt-connector/src/data/Community.repository.ts new file mode 100644 index 000000000..78023b15e --- /dev/null +++ b/dlt-connector/src/data/Community.repository.ts @@ -0,0 +1,76 @@ +import { Community } from '@entity/Community' +import { FindOptionsSelect, In, IsNull, Not } from 'typeorm' + +import { CommunityArg } from '@/graphql/arg/CommunityArg' +import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' +import { CommunityDraft } from '@/graphql/input/CommunityDraft' +import { UserIdentifier } from '@/graphql/input/UserIdentifier' +import { TransactionError } from '@/graphql/model/TransactionError' +import { LogError } from '@/server/LogError' +import { getDataSource } from '@/typeorm/DataSource' +import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter' + +import { KeyPair } from './KeyPair' + +export const CommunityRepository = getDataSource() + .getRepository(Community) + .extend({ + async isExist(community: CommunityDraft | string): Promise { + const iotaTopic = + community instanceof CommunityDraft ? iotaTopicFromCommunityUUID(community.uuid) : community + const result = await this.find({ + where: { iotaTopic }, + }) + return result.length > 0 + }, + + async findByCommunityArg({ uuid, foreign, confirmed }: CommunityArg): Promise { + return await this.find({ + where: { + ...(uuid && { iotaTopic: iotaTopicFromCommunityUUID(uuid) }), + ...(foreign && { foreign }), + ...(confirmed && { confirmedAt: Not(IsNull()) }), + }, + }) + }, + + async findByCommunityUuid(communityUuid: string): Promise { + return await this.findOneBy({ iotaTopic: iotaTopicFromCommunityUUID(communityUuid) }) + }, + + async findByIotaTopic(iotaTopic: string): Promise { + return await this.findOneBy({ iotaTopic }) + }, + + findCommunitiesByTopics(topics: string[]): Promise { + return this.findBy({ iotaTopic: In(topics) }) + }, + + async getCommunityForUserIdentifier( + identifier: UserIdentifier, + ): Promise { + if (!identifier.communityUuid) { + throw new TransactionError(TransactionErrorType.MISSING_PARAMETER, 'community uuid not set') + } + return ( + (await this.findOneBy({ + iotaTopic: iotaTopicFromCommunityUUID(identifier.communityUuid), + })) ?? undefined + ) + }, + + findAll(select: FindOptionsSelect): Promise { + return this.find({ select }) + }, + + async loadHomeCommunityKeyPair(): Promise { + const community = await this.findOneOrFail({ + where: { foreign: false }, + select: { rootChaincode: true, rootPubkey: true, rootPrivkey: true }, + }) + if (!community.rootChaincode || !community.rootPrivkey) { + throw new LogError('Missing chaincode or private key for home community') + } + return new KeyPair(community) + }, + }) diff --git a/dlt-connector/src/data/KeyPair.ts b/dlt-connector/src/data/KeyPair.ts new file mode 100644 index 000000000..59e9a5066 --- /dev/null +++ b/dlt-connector/src/data/KeyPair.ts @@ -0,0 +1,87 @@ +import { Community } from '@entity/Community' + +// https://www.npmjs.com/package/bip32-ed25519 +import { LogError } from '@/server/LogError' + +import { toPublic, derivePrivate, sign, verify, generateFromSeed } from 'bip32-ed25519' + +import { Mnemonic } from './Mnemonic' + +/** + * Class Managing Key Pair and also generate, sign and verify signature with it + */ +export class KeyPair { + private _publicKey: Buffer + private _chainCode: Buffer + private _privateKey: Buffer + + /** + * @param input: Mnemonic = Mnemonic or Passphrase which work as seed for generating algorithms + * @param input: Buffer = extended private key, returned from bip32-ed25519 generateFromSeed or from derivePrivate + * @param input: Community = community entity with keys loaded from db + * + */ + public constructor(input: Mnemonic | Buffer | Community) { + if (input instanceof Mnemonic) { + this.loadFromExtendedPrivateKey(generateFromSeed(input.seed)) + } else if (input instanceof Buffer) { + this.loadFromExtendedPrivateKey(input) + } else if (input instanceof Community) { + if (!input.rootPrivkey || !input.rootChaincode || !input.rootPubkey) { + throw new LogError('missing private key or chaincode or public key in commmunity entity') + } + this._privateKey = input.rootPrivkey + this._publicKey = input.rootPubkey + this._chainCode = input.rootChaincode + } + } + + /** + * copy keys to community entity + * @param community + */ + public fillInCommunityKeys(community: Community) { + community.rootPubkey = this._publicKey + community.rootPrivkey = this._privateKey + community.rootChaincode = this._chainCode + } + + private loadFromExtendedPrivateKey(extendedPrivateKey: Buffer) { + if (extendedPrivateKey.length !== 96) { + throw new LogError('invalid extended private key') + } + this._privateKey = extendedPrivateKey.subarray(0, 64) + this._chainCode = extendedPrivateKey.subarray(64, 96) + this._publicKey = toPublic(extendedPrivateKey).subarray(0, 32) + } + + public getExtendPrivateKey(): Buffer { + return Buffer.concat([this._privateKey, this._chainCode]) + } + + public getExtendPublicKey(): Buffer { + return Buffer.concat([this._publicKey, this._chainCode]) + } + + public get publicKey(): Buffer { + return this._publicKey + } + + public derive(path: number[]): KeyPair { + const extendedPrivateKey = this.getExtendPrivateKey() + return new KeyPair( + path.reduce( + (extendPrivateKey: Buffer, node: number) => derivePrivate(extendPrivateKey, node), + extendedPrivateKey, + ), + ) + } + + public sign(message: Buffer): Buffer { + return sign(message, this.getExtendPrivateKey()) + } + + public verify(message: Buffer, signature: Buffer): boolean { + return verify(message, signature, this.getExtendPublicKey()) + } +} diff --git a/dlt-connector/src/data/Mnemonic.ts b/dlt-connector/src/data/Mnemonic.ts new file mode 100644 index 000000000..8f15c1046 --- /dev/null +++ b/dlt-connector/src/data/Mnemonic.ts @@ -0,0 +1,25 @@ +// https://www.npmjs.com/package/bip39 +import { entropyToMnemonic, mnemonicToSeedSync } from 'bip39' +// eslint-disable-next-line camelcase +import { randombytes_buf } from 'sodium-native' + +export class Mnemonic { + private _passphrase = '' + public constructor(seed?: Buffer | string) { + if (seed) { + this._passphrase = entropyToMnemonic(seed) + return + } + const entropy = Buffer.alloc(256) + randombytes_buf(entropy) + this._passphrase = entropyToMnemonic(entropy) + } + + public get passphrase(): string { + return this._passphrase + } + + public get seed(): Buffer { + return mnemonicToSeedSync(this._passphrase) + } +} diff --git a/dlt-connector/src/data/Transaction.builder.ts b/dlt-connector/src/data/Transaction.builder.ts new file mode 100644 index 000000000..115391e91 --- /dev/null +++ b/dlt-connector/src/data/Transaction.builder.ts @@ -0,0 +1,179 @@ +import { Account } from '@entity/Account' +import { Community } from '@entity/Community' +import { Transaction } from '@entity/Transaction' + +import { GradidoTransaction } from '@/data/proto/3_3/GradidoTransaction' +import { TransactionBody } from '@/data/proto/3_3/TransactionBody' +import { TransactionDraft } from '@/graphql/input/TransactionDraft' +import { UserIdentifier } from '@/graphql/input/UserIdentifier' +import { LogError } from '@/server/LogError' +import { bodyBytesToTransactionBody, transactionBodyToBodyBytes } from '@/utils/typeConverter' + +import { AccountRepository } from './Account.repository' +import { BackendTransactionFactory } from './BackendTransaction.factory' +import { CommunityRepository } from './Community.repository' +import { TransactionBodyBuilder } from './proto/TransactionBody.builder' + +export class TransactionBuilder { + private transaction: Transaction + + // https://refactoring.guru/design-patterns/builder/typescript/example + /** + * A fresh builder instance should contain a blank product object, which is + * used in further assembly. + */ + constructor() { + this.reset() + } + + public reset(): void { + this.transaction = Transaction.create() + } + + /** + * Concrete Builders are supposed to provide their own methods for + * retrieving results. That's because various types of builders may create + * entirely different products that don't follow the same interface. + * Therefore, such methods cannot be declared in the base Builder interface + * (at least in a statically typed programming language). + * + * Usually, after returning the end result to the client, a builder instance + * is expected to be ready to start producing another product. That's why + * it's a usual practice to call the reset method at the end of the + * `getProduct` method body. However, this behavior is not mandatory, and + * you can make your builders wait for an explicit reset call from the + * client code before disposing of the previous result. + */ + public build(): Transaction { + const result = this.transaction + this.reset() + return result + } + + // return transaction without calling reset + public getTransaction(): Transaction { + return this.transaction + } + + public getCommunity(): Community { + return this.transaction.community + } + + public setSigningAccount(signingAccount: Account): TransactionBuilder { + this.transaction.signingAccount = signingAccount + return this + } + + public setRecipientAccount(recipientAccount: Account): TransactionBuilder { + this.transaction.recipientAccount = recipientAccount + return this + } + + public setCommunity(community: Community): TransactionBuilder { + this.transaction.community = community + return this + } + + public setOtherCommunity(otherCommunity?: Community): TransactionBuilder { + if (!this.transaction.community) { + throw new LogError('Please set community first!') + } + + this.transaction.otherCommunity = + otherCommunity && + this.transaction.community && + this.transaction.community.id !== otherCommunity.id + ? otherCommunity + : undefined + return this + } + + public setSignature(signature: Buffer): TransactionBuilder { + this.transaction.signature = signature + return this + } + + public addBackendTransaction(transactionDraft: TransactionDraft): TransactionBuilder { + if (!this.transaction.backendTransactions) { + this.transaction.backendTransactions = [] + } + this.transaction.backendTransactions.push( + BackendTransactionFactory.createFromTransactionDraft(transactionDraft), + ) + return this + } + + public async setSenderCommunityFromSenderUser( + senderUser: UserIdentifier, + ): Promise { + // get sender community + const community = await CommunityRepository.getCommunityForUserIdentifier(senderUser) + if (!community) { + throw new LogError("couldn't find community for transaction") + } + return this.setCommunity(community) + } + + public async setOtherCommunityFromRecipientUser( + recipientUser: UserIdentifier, + ): Promise { + // get recipient community + const otherCommunity = await CommunityRepository.getCommunityForUserIdentifier(recipientUser) + return this.setOtherCommunity(otherCommunity) + } + + public async fromGradidoTransactionSearchForAccounts( + gradidoTransaction: GradidoTransaction, + ): Promise { + this.transaction.bodyBytes = Buffer.from(gradidoTransaction.bodyBytes) + const transactionBody = bodyBytesToTransactionBody(this.transaction.bodyBytes) + this.fromTransactionBody(transactionBody) + + const firstSigPair = gradidoTransaction.getFirstSignature() + // TODO: adapt if transactions with more than one signatures where added + + // get recipient and signer accounts if not already set + this.transaction.signingAccount ??= await AccountRepository.findAccountByPublicKey( + firstSigPair.pubKey, + ) + this.transaction.recipientAccount ??= await AccountRepository.findAccountByPublicKey( + transactionBody.getRecipientPublicKey(), + ) + this.transaction.signature = Buffer.from(firstSigPair.signature) + + return this + } + + public fromGradidoTransaction(gradidoTransaction: GradidoTransaction): TransactionBuilder { + this.transaction.bodyBytes = Buffer.from(gradidoTransaction.bodyBytes) + const transactionBody = bodyBytesToTransactionBody(this.transaction.bodyBytes) + this.fromTransactionBody(transactionBody) + + const firstSigPair = gradidoTransaction.getFirstSignature() + // TODO: adapt if transactions with more than one signatures where added + this.transaction.signature = Buffer.from(firstSigPair.signature) + + return this + } + + public fromTransactionBody(transactionBody: TransactionBody): TransactionBuilder { + transactionBody.fillTransactionRecipe(this.transaction) + this.transaction.bodyBytes ??= transactionBodyToBodyBytes(transactionBody) + return this + } + + public fromTransactionBodyBuilder( + transactionBodyBuilder: TransactionBodyBuilder, + ): TransactionBuilder { + const signingAccount = transactionBodyBuilder.getSigningAccount() + if (signingAccount) { + this.setSigningAccount(signingAccount) + } + const recipientAccount = transactionBodyBuilder.getRecipientAccount() + if (recipientAccount) { + this.setRecipientAccount(recipientAccount) + } + this.fromTransactionBody(transactionBodyBuilder.getTransactionBody()) + return this + } +} diff --git a/dlt-connector/src/data/Transaction.repository.ts b/dlt-connector/src/data/Transaction.repository.ts new file mode 100644 index 000000000..6ba622c9c --- /dev/null +++ b/dlt-connector/src/data/Transaction.repository.ts @@ -0,0 +1,43 @@ +import { Transaction } from '@entity/Transaction' +import { IsNull } from 'typeorm' + +import { getDataSource } from '@/typeorm/DataSource' + +// https://www.artima.com/articles/the-dci-architecture-a-new-vision-of-object-oriented-programming +export const TransactionRepository = getDataSource() + .getRepository(Transaction) + .extend({ + findBySignature(signature: Buffer): Promise { + return this.findOneBy({ signature: Buffer.from(signature) }) + }, + findByMessageId(iotaMessageId: string): Promise { + return this.findOneBy({ iotaMessageId: Buffer.from(iotaMessageId, 'hex') }) + }, + async getNextPendingTransaction(): Promise { + return await this.findOne({ + where: { iotaMessageId: IsNull() }, + order: { createdAt: 'ASC' }, + relations: { signingAccount: true }, + }) + }, + findExistingTransactionAndMissingMessageIds(messageIDsHex: string[]): Promise { + return this.createQueryBuilder('Transaction') + .where('HEX(Transaction.iota_message_id) IN (:...messageIDs)', { + messageIDs: messageIDsHex, + }) + .leftJoinAndSelect('Transaction.community', 'Community') + .leftJoinAndSelect('Transaction.otherCommunity', 'OtherCommunity') + .leftJoinAndSelect('Transaction.recipientAccount', 'RecipientAccount') + .leftJoinAndSelect('Transaction.backendTransactions', 'BackendTransactions') + .leftJoinAndSelect('RecipientAccount.user', 'RecipientUser') + .leftJoinAndSelect('Transaction.signingAccount', 'SigningAccount') + .leftJoinAndSelect('SigningAccount.user', 'SigningUser') + .getMany() + }, + removeConfirmedTransaction(transactions: Transaction[]): Transaction[] { + return transactions.filter( + (transaction: Transaction) => + transaction.runningHash === undefined || transaction.runningHash.length === 0, + ) + }, + }) diff --git a/dlt-connector/src/data/User.factory.ts b/dlt-connector/src/data/User.factory.ts new file mode 100644 index 000000000..a8c7f0e71 --- /dev/null +++ b/dlt-connector/src/data/User.factory.ts @@ -0,0 +1,18 @@ +import { User } from '@entity/User' + +import { UserAccountDraft } from '@/graphql/input/UserAccountDraft' + +import { KeyPair } from './KeyPair' +import { UserLogic } from './User.logic' + +export class UserFactory { + static create(userAccountDraft: UserAccountDraft, parentKeys: KeyPair): User { + const user = User.create() + user.createdAt = new Date(userAccountDraft.createdAt) + user.gradidoID = userAccountDraft.user.uuid + const userLogic = new UserLogic(user) + // store generated pubkey into entity + userLogic.calculateKeyPair(parentKeys) + return user + } +} diff --git a/dlt-connector/src/data/User.logic.ts b/dlt-connector/src/data/User.logic.ts new file mode 100644 index 000000000..0a906682d --- /dev/null +++ b/dlt-connector/src/data/User.logic.ts @@ -0,0 +1,42 @@ +import { User } from '@entity/User' + +import { LogError } from '@/server/LogError' +import { hardenDerivationIndex } from '@/utils/derivationHelper' +import { uuid4ToBuffer } from '@/utils/typeConverter' + +import { KeyPair } from './KeyPair' + +export class UserLogic { + // eslint-disable-next-line no-useless-constructor + constructor(private user: User) {} + + /** + * + * @param parentKeys if undefined use home community key pair + * @returns + */ + + calculateKeyPair = (parentKeys: KeyPair): KeyPair => { + if (!this.user.gradidoID) { + throw new LogError('missing GradidoID for user.', { id: this.user.id }) + } + // example gradido id: 03857ac1-9cc2-483e-8a91-e5b10f5b8d16 => + // wholeHex: '03857ac19cc2483e8a91e5b10f5b8d16'] + const wholeHex = uuid4ToBuffer(this.user.gradidoID) + const parts = [] + for (let i = 0; i < 4; i++) { + parts[i] = hardenDerivationIndex(wholeHex.subarray(i * 4, (i + 1) * 4).readUInt32BE()) + } + // parts: [2206563009, 2629978174, 2324817329, 2405141782] + const keyPair = parentKeys.derive(parts) + if (this.user.derive1Pubkey && this.user.derive1Pubkey.compare(keyPair.publicKey) !== 0) { + throw new LogError( + 'The freshly derived public key does not correspond to the stored public key', + ) + } + if (!this.user.derive1Pubkey) { + this.user.derive1Pubkey = keyPair.publicKey + } + return keyPair + } +} diff --git a/dlt-connector/src/data/User.repository.ts b/dlt-connector/src/data/User.repository.ts new file mode 100644 index 000000000..6e5a66203 --- /dev/null +++ b/dlt-connector/src/data/User.repository.ts @@ -0,0 +1,24 @@ +import { Account } from '@entity/Account' +import { User } from '@entity/User' + +import { UserIdentifier } from '@/graphql/input/UserIdentifier' +import { getDataSource } from '@/typeorm/DataSource' + +export const UserRepository = getDataSource() + .getRepository(User) + .extend({ + async findAccountByUserIdentifier({ + uuid, + accountNr, + }: UserIdentifier): Promise { + const user = await this.findOne({ + where: { gradidoID: uuid, accounts: { derivationIndex: accountNr ?? 1 } }, + relations: { accounts: true }, + }) + if (user && user.accounts?.length === 1) { + const account = user.accounts[0] + account.user = user + return account + } + }, + }) diff --git a/dlt-connector/src/data/proto/3_3/CommunityRoot.ts b/dlt-connector/src/data/proto/3_3/CommunityRoot.ts new file mode 100644 index 000000000..c03460741 --- /dev/null +++ b/dlt-connector/src/data/proto/3_3/CommunityRoot.ts @@ -0,0 +1,35 @@ +import { Community } from '@entity/Community' +import { Transaction } from '@entity/Transaction' +import { Field, Message } from 'protobufjs' + +import { AbstractTransaction } from '../AbstractTransaction' + +// https://www.npmjs.com/package/@apollo/protobufjs +// eslint-disable-next-line no-use-before-define +export class CommunityRoot extends Message implements AbstractTransaction { + public constructor(community?: Community) { + if (community) { + super({ + rootPubkey: community.rootPubkey, + gmwPubkey: community.gmwAccount?.derive2Pubkey, + aufPubkey: community.aufAccount?.derive2Pubkey, + }) + } else { + super() + } + } + + @Field.d(1, 'bytes') + public rootPubkey: Buffer + + // community public budget account + @Field.d(2, 'bytes') + public gmwPubkey: Buffer + + // community compensation and environment founds account + @Field.d(3, 'bytes') + public aufPubkey: Buffer + + // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars + public fillTransactionRecipe(recipe: Transaction): void {} +} diff --git a/dlt-connector/src/proto/3_3/GradidoConfirmedTransaction.ts b/dlt-connector/src/data/proto/3_3/ConfirmedTransaction.ts similarity index 69% rename from dlt-connector/src/proto/3_3/GradidoConfirmedTransaction.ts rename to dlt-connector/src/data/proto/3_3/ConfirmedTransaction.ts index 7f0a58109..d59b991e8 100644 --- a/dlt-connector/src/proto/3_3/GradidoConfirmedTransaction.ts +++ b/dlt-connector/src/data/proto/3_3/ConfirmedTransaction.ts @@ -1,4 +1,7 @@ -import { Field, Message } from '@apollo/protobufjs' +import { Field, Message } from 'protobufjs' + +import { base64ToBuffer } from '@/utils/typeConverter' + import { GradidoTransaction } from './GradidoTransaction' import { TimestampSeconds } from './TimestampSeconds' @@ -10,9 +13,13 @@ import { TimestampSeconds } from './TimestampSeconds' // https://www.npmjs.com/package/@apollo/protobufjs // eslint-disable-next-line no-use-before-define -export class GradidoConfirmedTransaction extends Message { +export class ConfirmedTransaction extends Message { + static fromBase64(base64: string): ConfirmedTransaction { + return ConfirmedTransaction.decode(new Uint8Array(base64ToBuffer(base64))) + } + @Field.d(1, 'uint64') - id: number + id: Long @Field.d(2, 'GradidoTransaction') transaction: GradidoTransaction diff --git a/dlt-connector/src/proto/3_3/GradidoCreation.test.ts b/dlt-connector/src/data/proto/3_3/GradidoCreation.test.ts similarity index 99% rename from dlt-connector/src/proto/3_3/GradidoCreation.test.ts rename to dlt-connector/src/data/proto/3_3/GradidoCreation.test.ts index 8b3fd1b3f..06011838c 100644 --- a/dlt-connector/src/proto/3_3/GradidoCreation.test.ts +++ b/dlt-connector/src/data/proto/3_3/GradidoCreation.test.ts @@ -1,9 +1,11 @@ import 'reflect-metadata' -import { TransactionDraft } from '@/graphql/input/TransactionDraft' -import { GradidoCreation } from './GradidoCreation' -import { TransactionError } from '@/graphql/model/TransactionError' import { TransactionErrorType } from '@enum/TransactionErrorType' +import { TransactionDraft } from '@/graphql/input/TransactionDraft' +import { TransactionError } from '@/graphql/model/TransactionError' + +import { GradidoCreation } from './GradidoCreation' + describe('proto/3.3/GradidoCreation', () => { it('test with missing targetDate', () => { const transactionDraft = new TransactionDraft() diff --git a/dlt-connector/src/data/proto/3_3/GradidoCreation.ts b/dlt-connector/src/data/proto/3_3/GradidoCreation.ts new file mode 100644 index 000000000..0fa08eff5 --- /dev/null +++ b/dlt-connector/src/data/proto/3_3/GradidoCreation.ts @@ -0,0 +1,53 @@ +import { Account } from '@entity/Account' +import { Transaction } from '@entity/Transaction' +import { Decimal } from 'decimal.js-light' +import { Field, Message } from 'protobufjs' + +import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' +import { TransactionDraft } from '@/graphql/input/TransactionDraft' +import { TransactionError } from '@/graphql/model/TransactionError' + +import { AbstractTransaction } from '../AbstractTransaction' + +import { TimestampSeconds } from './TimestampSeconds' +import { TransferAmount } from './TransferAmount' + +// need signature from group admin or +// percent of group users another than the receiver +// https://www.npmjs.com/package/@apollo/protobufjs +// eslint-disable-next-line no-use-before-define +export class GradidoCreation extends Message implements AbstractTransaction { + constructor(transaction?: TransactionDraft, recipientAccount?: Account) { + if (transaction) { + if (!transaction.targetDate) { + throw new TransactionError( + TransactionErrorType.MISSING_PARAMETER, + 'missing targetDate for contribution', + ) + } + super({ + recipient: new TransferAmount({ + amount: transaction.amount.toString(), + pubkey: recipientAccount?.derive2Pubkey, + }), + targetDate: new TimestampSeconds(new Date(transaction.targetDate)), + }) + } else { + super() + } + } + + // recipient: TransferAmount contain + // - recipient public key + // - amount + // - communityId // only set if not the same as recipient community + @Field.d(1, TransferAmount) + public recipient: TransferAmount + + @Field.d(3, 'TimestampSeconds') + public targetDate: TimestampSeconds + + public fillTransactionRecipe(recipe: Transaction): void { + recipe.amount = new Decimal(this.recipient.amount ?? 0) + } +} diff --git a/dlt-connector/src/proto/3_3/GradidoDeferredTransfer.ts b/dlt-connector/src/data/proto/3_3/GradidoDeferredTransfer.ts similarity index 73% rename from dlt-connector/src/proto/3_3/GradidoDeferredTransfer.ts rename to dlt-connector/src/data/proto/3_3/GradidoDeferredTransfer.ts index 7b27c064a..f48719b16 100644 --- a/dlt-connector/src/proto/3_3/GradidoDeferredTransfer.ts +++ b/dlt-connector/src/data/proto/3_3/GradidoDeferredTransfer.ts @@ -1,4 +1,8 @@ -import { Field, Message } from '@apollo/protobufjs' +import { Transaction } from '@entity/Transaction' +import Decimal from 'decimal.js-light' +import { Field, Message } from 'protobufjs' + +import { AbstractTransaction } from '../AbstractTransaction' import { GradidoTransfer } from './GradidoTransfer' import { TimestampSeconds } from './TimestampSeconds' @@ -10,8 +14,11 @@ import { TimestampSeconds } from './TimestampSeconds' // seed must be long enough to prevent brute force, maybe base64 encoded // to own account // https://www.npmjs.com/package/@apollo/protobufjs -// eslint-disable-next-line no-use-before-define -export class GradidoDeferredTransfer extends Message { +export class GradidoDeferredTransfer + // eslint-disable-next-line no-use-before-define + extends Message + implements AbstractTransaction +{ // amount is amount with decay for time span between transaction was received and timeout // useable amount can be calculated // recipient address don't need to be registered in blockchain with register address @@ -28,4 +35,8 @@ export class GradidoDeferredTransfer extends Message { // split for n recipient // max gradido per recipient? or per transaction with cool down? + + public fillTransactionRecipe(recipe: Transaction): void { + recipe.amount = new Decimal(this.transfer.sender.amount ?? 0) + } } diff --git a/dlt-connector/src/data/proto/3_3/GradidoTransaction.ts b/dlt-connector/src/data/proto/3_3/GradidoTransaction.ts new file mode 100644 index 000000000..f38bcbd1f --- /dev/null +++ b/dlt-connector/src/data/proto/3_3/GradidoTransaction.ts @@ -0,0 +1,59 @@ +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' +import { SignaturePair } from './SignaturePair' +import { TransactionBody } from './TransactionBody' + +// https://www.npmjs.com/package/@apollo/protobufjs +// eslint-disable-next-line no-use-before-define +export class GradidoTransaction extends Message { + constructor(body?: TransactionBody) { + if (body) { + super({ + sigMap: new SignatureMap(), + bodyBytes: Buffer.from(TransactionBody.encode(body).finish()), + }) + } else { + super() + } + } + + @Field.d(1, SignatureMap) + public sigMap: SignatureMap + + // inspired by Hedera + // bodyBytes are the payload for signature + // bodyBytes are serialized TransactionBody + @Field.d(2, 'bytes') + public bodyBytes: Buffer + + // if it is a cross group transaction the parent message + // id from outbound transaction or other by cross + @Field.d(3, 'bytes') + public parentMessageId?: Buffer + + getFirstSignature(): SignaturePair { + const sigPair = this.sigMap.sigPair + if (sigPair.length !== 1) { + throw new LogError("signature count don't like expected") + } + return sigPair[0] + } + + 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', + ) + } + } +} diff --git a/dlt-connector/src/data/proto/3_3/GradidoTransfer.ts b/dlt-connector/src/data/proto/3_3/GradidoTransfer.ts new file mode 100644 index 000000000..7e9da40bd --- /dev/null +++ b/dlt-connector/src/data/proto/3_3/GradidoTransfer.ts @@ -0,0 +1,49 @@ +import { Account } from '@entity/Account' +import { Transaction } from '@entity/Transaction' +import Decimal from 'decimal.js-light' +import { Field, Message } from 'protobufjs' + +import { TransactionDraft } from '@/graphql/input/TransactionDraft' + +import { AbstractTransaction } from '../AbstractTransaction' + +import { TransferAmount } from './TransferAmount' + +// https://www.npmjs.com/package/@apollo/protobufjs +// eslint-disable-next-line no-use-before-define +export class GradidoTransfer extends Message implements AbstractTransaction { + constructor( + transaction?: TransactionDraft, + signingAccount?: Account, + recipientAccount?: Account, + coinOrigin?: string, + ) { + if (transaction) { + super({ + sender: new TransferAmount({ + amount: transaction.amount.toString(), + pubkey: signingAccount?.derive2Pubkey, + communityId: coinOrigin, + }), + recipient: recipientAccount?.derive2Pubkey, + }) + } else { + super() + } + } + + // sender: TransferAmount contain + // - sender public key + // - amount + // - communityId // only set if not the same as sender and recipient community + @Field.d(1, TransferAmount) + public sender: TransferAmount + + // the recipient public key + @Field.d(2, 'bytes') + public recipient: Buffer + + public fillTransactionRecipe(recipe: Transaction): void { + recipe.amount = new Decimal(this.sender?.amount ?? 0) + } +} diff --git a/dlt-connector/src/proto/3_3/GroupFriendsUpdate.ts b/dlt-connector/src/data/proto/3_3/GroupFriendsUpdate.ts similarity index 62% rename from dlt-connector/src/proto/3_3/GroupFriendsUpdate.ts rename to dlt-connector/src/data/proto/3_3/GroupFriendsUpdate.ts index 64e74a694..b64e80a73 100644 --- a/dlt-connector/src/proto/3_3/GroupFriendsUpdate.ts +++ b/dlt-connector/src/data/proto/3_3/GroupFriendsUpdate.ts @@ -1,10 +1,14 @@ -import { Field, Message } from '@apollo/protobufjs' +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Transaction } from '@entity/Transaction' +import { Field, Message } from 'protobufjs' + +import { AbstractTransaction } from '../AbstractTransaction' // connect group together // only CrossGroupType CROSS (in TransactionBody) // https://www.npmjs.com/package/@apollo/protobufjs // eslint-disable-next-line no-use-before-define -export class GroupFriendsUpdate extends Message { +export class GroupFriendsUpdate extends Message implements AbstractTransaction { // if set to true, colors of this both groups are trait as the same // on creation user get coins still in there color // on transfer into another group which a connection exist, @@ -12,4 +16,8 @@ export class GroupFriendsUpdate extends Message { // (if fusion between src coin and dst coin is enabled) @Field.d(1, 'bool') public colorFusion: boolean + + public fillTransactionRecipe(recipe: Transaction): void { + throw new Error('Method not implemented.') + } } diff --git a/dlt-connector/src/data/proto/3_3/RegisterAddress.ts b/dlt-connector/src/data/proto/3_3/RegisterAddress.ts new file mode 100644 index 000000000..87f09afbd --- /dev/null +++ b/dlt-connector/src/data/proto/3_3/RegisterAddress.ts @@ -0,0 +1,29 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Transaction } from '@entity/Transaction' +import { Field, Message } from 'protobufjs' + +import { AddressType } from '@/data/proto/3_3/enum/AddressType' + +import { AbstractTransaction } from '../AbstractTransaction' + +// https://www.npmjs.com/package/@apollo/protobufjs +// eslint-disable-next-line no-use-before-define +export class RegisterAddress extends Message implements AbstractTransaction { + @Field.d(1, 'bytes') + public userPubkey: Buffer + + @Field.d(2, AddressType) + public addressType: AddressType + + @Field.d(3, 'bytes') + public nameHash: Buffer + + @Field.d(4, 'bytes') + public accountPubkey: Buffer + + @Field.d(5, 'uint32') + public derivationIndex?: number + + public fillTransactionRecipe(_recipe: Transaction): void {} +} diff --git a/dlt-connector/src/proto/3_3/SignatureMap.ts b/dlt-connector/src/data/proto/3_3/SignatureMap.ts similarity index 66% rename from dlt-connector/src/proto/3_3/SignatureMap.ts rename to dlt-connector/src/data/proto/3_3/SignatureMap.ts index e48b0232d..daf69f05f 100644 --- a/dlt-connector/src/proto/3_3/SignatureMap.ts +++ b/dlt-connector/src/data/proto/3_3/SignatureMap.ts @@ -1,10 +1,14 @@ -import { Field, Message } from '@apollo/protobufjs' +import { Field, Message } from 'protobufjs' import { SignaturePair } from './SignaturePair' // https://www.npmjs.com/package/@apollo/protobufjs // eslint-disable-next-line no-use-before-define export class SignatureMap extends Message { + constructor() { + super({ sigPair: [] }) + } + @Field.d(1, SignaturePair, 'repeated') - public sigPair: SignaturePair + public sigPair: SignaturePair[] } diff --git a/dlt-connector/src/proto/3_3/SignaturePair.ts b/dlt-connector/src/data/proto/3_3/SignaturePair.ts similarity index 63% rename from dlt-connector/src/proto/3_3/SignaturePair.ts rename to dlt-connector/src/data/proto/3_3/SignaturePair.ts index 07ed4cc55..80a61a871 100644 --- a/dlt-connector/src/proto/3_3/SignaturePair.ts +++ b/dlt-connector/src/data/proto/3_3/SignaturePair.ts @@ -1,4 +1,4 @@ -import { Field, Message } from '@apollo/protobufjs' +import { Field, Message } from 'protobufjs' // https://www.npmjs.com/package/@apollo/protobufjs // eslint-disable-next-line no-use-before-define @@ -8,4 +8,8 @@ export class SignaturePair extends Message { @Field.d(2, 'bytes') public signature: Buffer + + public validate(): boolean { + return this.pubKey.length === 32 && this.signature.length === 64 + } } diff --git a/dlt-connector/src/proto/3_3/Timestamp.test.ts b/dlt-connector/src/data/proto/3_3/Timestamp.test.ts similarity index 100% rename from dlt-connector/src/proto/3_3/Timestamp.test.ts rename to dlt-connector/src/data/proto/3_3/Timestamp.test.ts diff --git a/dlt-connector/src/proto/3_3/Timestamp.ts b/dlt-connector/src/data/proto/3_3/Timestamp.ts similarity index 94% rename from dlt-connector/src/proto/3_3/Timestamp.ts rename to dlt-connector/src/data/proto/3_3/Timestamp.ts index ab060a9bc..91cf06581 100644 --- a/dlt-connector/src/proto/3_3/Timestamp.ts +++ b/dlt-connector/src/data/proto/3_3/Timestamp.ts @@ -1,4 +1,4 @@ -import { Field, Message } from '@apollo/protobufjs' +import { Field, Message } from 'protobufjs' // https://www.npmjs.com/package/@apollo/protobufjs // eslint-disable-next-line no-use-before-define diff --git a/dlt-connector/src/proto/3_3/TimestampSeconds.test.ts b/dlt-connector/src/data/proto/3_3/TimestampSeconds.test.ts similarity index 100% rename from dlt-connector/src/proto/3_3/TimestampSeconds.test.ts rename to dlt-connector/src/data/proto/3_3/TimestampSeconds.test.ts diff --git a/dlt-connector/src/proto/3_3/TimestampSeconds.ts b/dlt-connector/src/data/proto/3_3/TimestampSeconds.ts similarity index 91% rename from dlt-connector/src/proto/3_3/TimestampSeconds.ts rename to dlt-connector/src/data/proto/3_3/TimestampSeconds.ts index 055094c6d..6d175c6f3 100644 --- a/dlt-connector/src/proto/3_3/TimestampSeconds.ts +++ b/dlt-connector/src/data/proto/3_3/TimestampSeconds.ts @@ -1,4 +1,4 @@ -import { Field, Message } from '@apollo/protobufjs' +import { Field, Message } from 'protobufjs' // https://www.npmjs.com/package/@apollo/protobufjs // eslint-disable-next-line no-use-before-define diff --git a/dlt-connector/src/data/proto/3_3/TransactionBody.ts b/dlt-connector/src/data/proto/3_3/TransactionBody.ts new file mode 100644 index 000000000..0c2733606 --- /dev/null +++ b/dlt-connector/src/data/proto/3_3/TransactionBody.ts @@ -0,0 +1,133 @@ +import { Transaction } from '@entity/Transaction' +import { Field, Message, OneOf } from 'protobufjs' + +import { CommunityDraft } from '@/graphql/input/CommunityDraft' +import { TransactionDraft } from '@/graphql/input/TransactionDraft' +import { LogError } from '@/server/LogError' +import { timestampToDate } from '@/utils/typeConverter' + +import { AbstractTransaction } from '../AbstractTransaction' + +import { CommunityRoot } from './CommunityRoot' +import { PROTO_TRANSACTION_BODY_VERSION_NUMBER } from './const' +import { CrossGroupType } from './enum/CrossGroupType' +import { TransactionType } from './enum/TransactionType' +import { GradidoCreation } from './GradidoCreation' +import { GradidoDeferredTransfer } from './GradidoDeferredTransfer' +import { GradidoTransfer } from './GradidoTransfer' +import { GroupFriendsUpdate } from './GroupFriendsUpdate' +import { RegisterAddress } from './RegisterAddress' +import { Timestamp } from './Timestamp' + +// https://www.npmjs.com/package/@apollo/protobufjs +// eslint-disable-next-line no-use-before-define +export class TransactionBody extends Message { + public constructor(transaction?: TransactionDraft | CommunityDraft) { + if (transaction) { + super({ + memo: 'Not implemented yet', + createdAt: new Timestamp(new Date(transaction.createdAt)), + versionNumber: PROTO_TRANSACTION_BODY_VERSION_NUMBER, + type: CrossGroupType.LOCAL, + otherGroup: '', + }) + } else { + super() + } + } + + @Field.d(1, 'string') + public memo: string + + @Field.d(2, Timestamp) + public createdAt: Timestamp + + @Field.d(3, 'string') + public versionNumber: string + + @Field.d(4, CrossGroupType) + public type: CrossGroupType + + @Field.d(5, 'string') + public otherGroup: string + + @OneOf.d( + 'gradidoTransfer', + 'gradidoCreation', + 'groupFriendsUpdate', + 'registerAddress', + 'gradidoDeferredTransfer', + 'communityRoot', + ) + public data: string + + @Field.d(6, 'GradidoTransfer') + transfer?: GradidoTransfer + + @Field.d(7, 'GradidoCreation') + creation?: GradidoCreation + + @Field.d(8, 'GroupFriendsUpdate') + groupFriendsUpdate?: GroupFriendsUpdate + + @Field.d(9, 'RegisterAddress') + registerAddress?: RegisterAddress + + @Field.d(10, 'GradidoDeferredTransfer') + deferredTransfer?: GradidoDeferredTransfer + + @Field.d(11, 'CommunityRoot') + communityRoot?: CommunityRoot + + public getTransactionType(): TransactionType | undefined { + if (this.transfer) return TransactionType.GRADIDO_TRANSFER + else if (this.creation) return TransactionType.GRADIDO_CREATION + else if (this.groupFriendsUpdate) return TransactionType.GROUP_FRIENDS_UPDATE + else if (this.registerAddress) return TransactionType.REGISTER_ADDRESS + else if (this.deferredTransfer) return TransactionType.GRADIDO_DEFERRED_TRANSFER + else if (this.communityRoot) return TransactionType.COMMUNITY_ROOT + } + + // The `TransactionBody` class utilizes Protobuf's `OneOf` field structure which, according to Protobuf documentation + // (https://protobuf.dev/programming-guides/proto3/#oneof), allows only one field within the group to be set at a time. + // Therefore, accessing the `getTransactionDetails()` method returns the first initialized value among the defined fields, + // each of which should be of type AbstractTransaction. It's important to note that due to the nature of Protobuf's `OneOf`, + // only one type from the defined options can be set within the object obtained from Protobuf. + // + // If multiple fields are set in a single object, the method `getTransactionDetails()` will return the first defined value + // based on the order of checks. Developers should handle this behavior according to the expected Protobuf structure. + public getTransactionDetails(): AbstractTransaction | undefined { + if (this.transfer) return this.transfer + if (this.creation) return this.creation + if (this.groupFriendsUpdate) return this.groupFriendsUpdate + if (this.registerAddress) return this.registerAddress + if (this.deferredTransfer) return this.deferredTransfer + if (this.communityRoot) return this.communityRoot + } + + public fillTransactionRecipe(recipe: Transaction): void { + recipe.createdAt = timestampToDate(this.createdAt) + recipe.protocolVersion = this.versionNumber + const transactionType = this.getTransactionType() + if (!transactionType) { + throw new LogError("invalid TransactionBody couldn't determine transaction type") + } + recipe.type = transactionType.valueOf() + this.getTransactionDetails()?.fillTransactionRecipe(recipe) + } + + public getRecipientPublicKey(): Buffer | undefined { + if (this.transfer) { + // this.transfer.recipient contains the publicKey of the recipient + return this.transfer.recipient + } + if (this.creation) { + return this.creation.recipient.pubkey + } + if (this.deferredTransfer) { + // this.deferredTransfer.transfer.recipient contains the publicKey of the recipient + return this.deferredTransfer.transfer.recipient + } + return undefined + } +} diff --git a/dlt-connector/src/proto/3_3/TransferAmount.ts b/dlt-connector/src/data/proto/3_3/TransferAmount.ts similarity index 88% rename from dlt-connector/src/proto/3_3/TransferAmount.ts rename to dlt-connector/src/data/proto/3_3/TransferAmount.ts index f6adc47ff..42da65256 100644 --- a/dlt-connector/src/proto/3_3/TransferAmount.ts +++ b/dlt-connector/src/data/proto/3_3/TransferAmount.ts @@ -1,4 +1,4 @@ -import { Field, Message } from '@apollo/protobufjs' +import { Field, Message } from 'protobufjs' // https://www.npmjs.com/package/@apollo/protobufjs // eslint-disable-next-line no-use-before-define diff --git a/dlt-connector/src/data/proto/3_3/const.ts b/dlt-connector/src/data/proto/3_3/const.ts new file mode 100644 index 000000000..9733e14a2 --- /dev/null +++ b/dlt-connector/src/data/proto/3_3/const.ts @@ -0,0 +1 @@ +export const PROTO_TRANSACTION_BODY_VERSION_NUMBER = '3.3' diff --git a/dlt-connector/src/graphql/enum/AddressType.ts b/dlt-connector/src/data/proto/3_3/enum/AddressType.ts similarity index 66% rename from dlt-connector/src/graphql/enum/AddressType.ts rename to dlt-connector/src/data/proto/3_3/enum/AddressType.ts index 26efd2825..eace1e022 100644 --- a/dlt-connector/src/graphql/enum/AddressType.ts +++ b/dlt-connector/src/data/proto/3_3/enum/AddressType.ts @@ -1,3 +1,8 @@ +/** + * Enum for protobuf + * used from RegisterAddress to determine account type + * master implementation: https://github.com/gradido/gradido_protocol/blob/master/proto/gradido/register_address.proto + */ export enum AddressType { NONE = 0, // if no address was found COMMUNITY_HUMAN = 1, // creation account for human diff --git a/dlt-connector/src/data/proto/3_3/enum/CrossGroupType.ts b/dlt-connector/src/data/proto/3_3/enum/CrossGroupType.ts new file mode 100644 index 000000000..fee592e57 --- /dev/null +++ b/dlt-connector/src/data/proto/3_3/enum/CrossGroupType.ts @@ -0,0 +1,22 @@ +/** + * Enum for protobuf + * Determine Cross Group type of Transactions + * LOCAL: no cross group transactions, sender and recipient community are the same, only one transaction + * INBOUND: cross group transaction, Inbound part. On recipient community chain. Recipient side by Transfer Transactions + * OUTBOUND: cross group transaction, Outbound part. On sender community chain. Sender side by Transfer Transactions + * CROSS: for cross group transaction which haven't a direction like group friend update + * master implementation: https://github.com/gradido/gradido_protocol/blob/master/proto/gradido/transaction_body.proto + * + * Transaction Handling differ from database focused backend + * In Backend for each transfer transaction there are always two entries in db, + * on for sender user and one for recipient user despite storing basically the same data two times + * In Blockchain Implementation there only two transactions on cross group transactions, one for + * the sender community chain, one for the recipient community chain + * if the transaction stay in the community there is only one transaction + */ +export enum CrossGroupType { + LOCAL = 0, + INBOUND = 1, + OUTBOUND = 2, + CROSS = 3, +} diff --git a/dlt-connector/src/data/proto/3_3/enum/TransactionType.ts b/dlt-connector/src/data/proto/3_3/enum/TransactionType.ts new file mode 100644 index 000000000..c50f33bec --- /dev/null +++ b/dlt-connector/src/data/proto/3_3/enum/TransactionType.ts @@ -0,0 +1,13 @@ +/** + * based on TransactionBody data oneOf + * https://github.com/gradido/gradido_protocol/blob/master/proto/gradido/transaction_body.proto + * for storing type in db as number + */ +export enum TransactionType { + GRADIDO_TRANSFER = 1, + GRADIDO_CREATION = 2, + GROUP_FRIENDS_UPDATE = 3, + REGISTER_ADDRESS = 4, + GRADIDO_DEFERRED_TRANSFER = 5, + COMMUNITY_ROOT = 6, +} diff --git a/dlt-connector/src/data/proto/AbstractTransaction.ts b/dlt-connector/src/data/proto/AbstractTransaction.ts new file mode 100644 index 000000000..ac089b096 --- /dev/null +++ b/dlt-connector/src/data/proto/AbstractTransaction.ts @@ -0,0 +1,5 @@ +import { Transaction } from '@entity/Transaction' + +export abstract class AbstractTransaction { + public abstract fillTransactionRecipe(recipe: Transaction): void +} diff --git a/dlt-connector/src/data/proto/TransactionBody.builder.ts b/dlt-connector/src/data/proto/TransactionBody.builder.ts new file mode 100644 index 000000000..22d943d48 --- /dev/null +++ b/dlt-connector/src/data/proto/TransactionBody.builder.ts @@ -0,0 +1,138 @@ +import { Account } from '@entity/Account' +import { Community } from '@entity/Community' + +import { InputTransactionType } from '@/graphql/enum/InputTransactionType' +import { CommunityDraft } from '@/graphql/input/CommunityDraft' +import { TransactionDraft } from '@/graphql/input/TransactionDraft' +import { LogError } from '@/server/LogError' + +import { CommunityRoot } from './3_3/CommunityRoot' +import { CrossGroupType } from './3_3/enum/CrossGroupType' +import { GradidoCreation } from './3_3/GradidoCreation' +import { GradidoTransfer } from './3_3/GradidoTransfer' +import { TransactionBody } from './3_3/TransactionBody' + +export class TransactionBodyBuilder { + private signingAccount?: Account + private recipientAccount?: Account + private body: TransactionBody | undefined + + // https://refactoring.guru/design-patterns/builder/typescript/example + /** + * A fresh builder instance should contain a blank product object, which is + * used in further assembly. + */ + constructor() { + this.reset() + } + + public reset(): void { + this.body = undefined + this.signingAccount = undefined + this.recipientAccount = undefined + } + + /** + * Concrete Builders are supposed to provide their own methods for + * retrieving results. That's because various types of builders may create + * entirely different products that don't follow the same interface. + * Therefore, such methods cannot be declared in the base Builder interface + * (at least in a statically typed programming language). + * + * Usually, after returning the end result to the client, a builder instance + * is expected to be ready to start producing another product. That's why + * it's a usual practice to call the reset method at the end of the + * `getProduct` method body. However, this behavior is not mandatory, and + * you can make your builders wait for an explicit reset call from the + * client code before disposing of the previous result. + */ + public build(): TransactionBody { + const result = this.getTransactionBody() + this.reset() + return result + } + + public getTransactionBody(): TransactionBody { + if (!this.body) { + throw new LogError( + 'cannot build Transaction Body, missing information, please call at least fromTransactionDraft or fromCommunityDraft', + ) + } + return this.body + } + + public getSigningAccount(): Account | undefined { + return this.signingAccount + } + + public getRecipientAccount(): Account | undefined { + return this.recipientAccount + } + + public setSigningAccount(signingAccount: Account): TransactionBodyBuilder { + this.signingAccount = signingAccount + return this + } + + public setRecipientAccount(recipientAccount: Account): TransactionBodyBuilder { + this.recipientAccount = recipientAccount + return this + } + + public setCrossGroupType(type: CrossGroupType): this { + if (!this.body) { + throw new LogError( + 'body is undefined, please call fromTransactionDraft or fromCommunityDraft before', + ) + } + this.body.type = type + return this + } + + public setOtherGroup(otherGroup: string): this { + if (!this.body) { + throw new LogError( + 'body is undefined, please call fromTransactionDraft or fromCommunityDraft before', + ) + } + this.body.otherGroup = otherGroup + return this + } + + public fromTransactionDraft(transactionDraft: TransactionDraft): TransactionBodyBuilder { + this.body = new TransactionBody(transactionDraft) + // TODO: load pubkeys for sender and recipient user from db + switch (transactionDraft.type) { + case InputTransactionType.CREATION: + if (!this.recipientAccount) { + throw new LogError('missing recipient account for creation transaction!') + } + this.body.creation = new GradidoCreation(transactionDraft, this.recipientAccount) + this.body.data = 'gradidoCreation' + break + case InputTransactionType.SEND: + case InputTransactionType.RECEIVE: + if (!this.recipientAccount || !this.signingAccount) { + throw new LogError('missing signing and/or recipient account for transfer transaction!') + } + this.body.transfer = new GradidoTransfer( + transactionDraft, + this.signingAccount, + this.recipientAccount, + ) + this.body.data = 'gradidoTransfer' + break + } + return this + } + + public fromCommunityDraft( + communityDraft: CommunityDraft, + community: Community, + ): TransactionBodyBuilder { + this.body = new TransactionBody(communityDraft) + this.body.communityRoot = new CommunityRoot(community) + this.body.data = 'communityRoot' + return this + } +} diff --git a/dlt-connector/src/graphql/arg/CommunityArg.ts b/dlt-connector/src/graphql/arg/CommunityArg.ts new file mode 100644 index 000000000..59f65943e --- /dev/null +++ b/dlt-connector/src/graphql/arg/CommunityArg.ts @@ -0,0 +1,19 @@ +// https://www.npmjs.com/package/@apollo/protobufjs + +import { IsBoolean, IsUUID } from 'class-validator' +import { ArgsType, Field } from 'type-graphql' + +@ArgsType() +export class CommunityArg { + @Field(() => String, { nullable: true }) + @IsUUID('4') + uuid?: string + + @Field(() => Boolean, { nullable: true }) + @IsBoolean() + foreign?: boolean + + @Field(() => Boolean, { nullable: true }) + @IsBoolean() + confirmed?: boolean +} diff --git a/dlt-connector/src/graphql/enum/AccountType.ts b/dlt-connector/src/graphql/enum/AccountType.ts new file mode 100644 index 000000000..810c89044 --- /dev/null +++ b/dlt-connector/src/graphql/enum/AccountType.ts @@ -0,0 +1,21 @@ +import { registerEnumType } from 'type-graphql' + +/** + * enum for graphql + * describe input account type in UserAccountDraft + * should have the same entries like enum AddressType from proto/enum folder + */ +export enum AccountType { + NONE = 'NONE', // if no address was found + COMMUNITY_HUMAN = 'COMMUNITY_HUMAN', // creation account for human + COMMUNITY_GMW = 'COMMUNITY_GMW', // community public budget account + COMMUNITY_AUF = 'COMMUNITY_AUF', // community compensation and environment founds account + COMMUNITY_PROJECT = 'COMMUNITY_PROJECT', // no creations allowed + SUBACCOUNT = 'SUBACCOUNT', // no creations allowed + CRYPTO_ACCOUNT = 'CRYPTO_ACCOUNT', // user control his keys, no creations +} + +registerEnumType(AccountType, { + name: 'AccountType', // this one is mandatory + description: 'Type of account', // this one is optional +}) diff --git a/dlt-connector/src/graphql/enum/CrossGroupType.ts b/dlt-connector/src/graphql/enum/CrossGroupType.ts deleted file mode 100644 index 13e968509..000000000 --- a/dlt-connector/src/graphql/enum/CrossGroupType.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum CrossGroupType { - LOCAL = 0, - INBOUND = 1, - OUTBOUND = 2, - // for cross group transaction which haven't a direction like group friend update - // CROSS = 3, -} diff --git a/dlt-connector/src/graphql/enum/InputTransactionType.ts b/dlt-connector/src/graphql/enum/InputTransactionType.ts new file mode 100755 index 000000000..41eeac6cb --- /dev/null +++ b/dlt-connector/src/graphql/enum/InputTransactionType.ts @@ -0,0 +1,14 @@ +import { registerEnumType } from 'type-graphql' + +// enum for graphql but with int because it is the same in backend +// for transaction type from backend +export enum InputTransactionType { + CREATION = 1, + SEND = 2, + RECEIVE = 3, +} + +registerEnumType(InputTransactionType, { + name: 'InputTransactionType', // this one is mandatory + description: 'Type of the transaction', // this one is optional +}) diff --git a/dlt-connector/src/graphql/enum/TransactionErrorType.ts b/dlt-connector/src/graphql/enum/TransactionErrorType.ts index 0e72292c1..1b01bc0da 100644 --- a/dlt-connector/src/graphql/enum/TransactionErrorType.ts +++ b/dlt-connector/src/graphql/enum/TransactionErrorType.ts @@ -1,10 +1,17 @@ import { registerEnumType } from 'type-graphql' +// enum for graphql +// error groups for resolver answers export enum TransactionErrorType { NOT_IMPLEMENTED_YET = 'Not Implemented yet', MISSING_PARAMETER = 'Missing parameter', ALREADY_EXIST = 'Already exist', DB_ERROR = 'DB Error', + PROTO_DECODE_ERROR = 'Proto Decode Error', + PROTO_ENCODE_ERROR = 'Proto Encode Error', + INVALID_SIGNATURE = 'Invalid Signature', + LOGIC_ERROR = 'Logic Error', + NOT_FOUND = 'Not found', } registerEnumType(TransactionErrorType, { diff --git a/dlt-connector/src/graphql/enum/TransactionType.ts b/dlt-connector/src/graphql/enum/TransactionType.ts deleted file mode 100755 index aaa5bf92e..000000000 --- a/dlt-connector/src/graphql/enum/TransactionType.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { registerEnumType } from 'type-graphql' - -export enum TransactionType { - CREATION = 1, - SEND = 2, - RECEIVE = 3, -} - -registerEnumType(TransactionType, { - name: 'TransactionType', // this one is mandatory - description: 'Type of the transaction', // this one is optional -}) diff --git a/dlt-connector/src/graphql/enum/TransactionValidationLevel.ts b/dlt-connector/src/graphql/enum/TransactionValidationLevel.ts deleted file mode 100644 index 9462dd8a8..000000000 --- a/dlt-connector/src/graphql/enum/TransactionValidationLevel.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { registerEnumType } from 'type-graphql' - -export enum TransactionValidationLevel { - SINGLE = 1, // check only the transaction - SINGLE_PREVIOUS = 2, // check also with previous transaction - DATE_RANGE = 3, // check all transaction from within date range by creation automatic the same month - PAIRED = 4, // check paired transaction on another group by cross group transactions - CONNECTED_GROUP = 5, // check all transactions in the group which connected with this transaction address(es) - CONNECTED_BLOCKCHAIN = 6, // check all transactions which connected with this transaction -} - -registerEnumType(TransactionValidationLevel, { - name: 'TransactionValidationLevel', - description: 'Transaction Validation Levels', -}) diff --git a/dlt-connector/src/graphql/input/CommunityDraft.ts b/dlt-connector/src/graphql/input/CommunityDraft.ts index f028ea06c..665e10b75 100644 --- a/dlt-connector/src/graphql/input/CommunityDraft.ts +++ b/dlt-connector/src/graphql/input/CommunityDraft.ts @@ -2,6 +2,7 @@ import { IsBoolean, IsUUID } from 'class-validator' import { Field, InputType } from 'type-graphql' + import { isValidDateString } from '@validator/DateString' @InputType() diff --git a/dlt-connector/src/graphql/input/TransactionDraft.ts b/dlt-connector/src/graphql/input/TransactionDraft.ts index 2021dd9e1..541797565 100755 --- a/dlt-connector/src/graphql/input/TransactionDraft.ts +++ b/dlt-connector/src/graphql/input/TransactionDraft.ts @@ -1,32 +1,37 @@ // https://www.npmjs.com/package/@apollo/protobufjs - +import { IsEnum, IsObject, IsPositive, ValidateNested } from 'class-validator' import { Decimal } from 'decimal.js-light' -import { TransactionType } from '@enum/TransactionType' -import { InputType, Field } from 'type-graphql' -import { UserIdentifier } from './UserIdentifier' +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, ValidateNested } from 'class-validator' + +import { UserIdentifier } from './UserIdentifier' @InputType() export class TransactionDraft { @Field(() => UserIdentifier) @IsObject() @ValidateNested() - senderUser: UserIdentifier + user: UserIdentifier @Field(() => UserIdentifier) @IsObject() @ValidateNested() - recipientUser: UserIdentifier + linkedUser: UserIdentifier + + @Field(() => Int) + @IsPositive() + backendTransactionId: number @Field(() => Decimal) @IsPositiveDecimal() amount: Decimal - @Field(() => TransactionType) - @IsEnum(TransactionType) - type: TransactionType + @Field(() => InputTransactionType) + @IsEnum(InputTransactionType) + type: InputTransactionType @Field(() => String) @isValidDateString() diff --git a/dlt-connector/src/graphql/input/TransactionInput.ts b/dlt-connector/src/graphql/input/TransactionInput.ts deleted file mode 100755 index d20b6ff27..000000000 --- a/dlt-connector/src/graphql/input/TransactionInput.ts +++ /dev/null @@ -1,27 +0,0 @@ -// https://www.npmjs.com/package/@apollo/protobufjs - -import { Decimal } from 'decimal.js-light' -import { TransactionType } from '../enum/TransactionType' -import { InputType, Field } from 'type-graphql' -import { IsEnum, IsInt, Min } from 'class-validator' -import { IsPositiveDecimal } from '../validator/Decimal' - -@InputType() -export class TransactionInput { - @Field(() => TransactionType) - @IsEnum(TransactionType) - type: TransactionType - - @Field(() => Decimal) - @IsPositiveDecimal() - amount: Decimal - - @Field(() => Number) - @IsInt() - @Min(978346800) - createdAt: number - - // @protoField.d(4, 'string') - // @Field(() => Decimal) - // communitySum: Decimal -} diff --git a/dlt-connector/src/graphql/input/UserAccountDraft.ts b/dlt-connector/src/graphql/input/UserAccountDraft.ts new file mode 100644 index 000000000..9ae544e32 --- /dev/null +++ b/dlt-connector/src/graphql/input/UserAccountDraft.ts @@ -0,0 +1,26 @@ +// https://www.npmjs.com/package/@apollo/protobufjs + +import { IsEnum, IsObject, ValidateNested } from 'class-validator' +import { InputType, Field } from 'type-graphql' + +import { isValidDateString } from '@validator/DateString' + +import { AccountType } from '@/graphql/enum/AccountType' + +import { UserIdentifier } from './UserIdentifier' + +@InputType() +export class UserAccountDraft { + @Field(() => UserIdentifier) + @IsObject() + @ValidateNested() + user: UserIdentifier + + @Field(() => String) + @isValidDateString() + createdAt: string + + @Field(() => AccountType) + @IsEnum(AccountType) + accountType: AccountType +} diff --git a/dlt-connector/src/graphql/model/Community.ts b/dlt-connector/src/graphql/model/Community.ts new file mode 100644 index 000000000..7a69288dc --- /dev/null +++ b/dlt-connector/src/graphql/model/Community.ts @@ -0,0 +1,36 @@ +import { Community as CommunityEntity } from '@entity/Community' +import { ObjectType, Field, Int } from 'type-graphql' + +@ObjectType() +export class Community { + constructor(entity: CommunityEntity) { + this.id = entity.id + this.iotaTopic = entity.iotaTopic + if (entity.rootPubkey) { + this.rootPublicKeyHex = entity.rootPubkey?.toString('hex') + } + this.foreign = entity.foreign + this.createdAt = entity.createdAt.toString() + if (entity.confirmedAt) { + this.confirmedAt = entity.confirmedAt.toString() + } + } + + @Field(() => Int) + id: number + + @Field(() => String) + iotaTopic: string + + @Field(() => String) + rootPublicKeyHex?: string + + @Field(() => Boolean) + foreign: boolean + + @Field(() => String) + createdAt: string + + @Field(() => String) + confirmedAt?: string +} diff --git a/dlt-connector/src/graphql/model/TransactionError.ts b/dlt-connector/src/graphql/model/TransactionError.ts index 891ad2a89..eee54e19c 100644 --- a/dlt-connector/src/graphql/model/TransactionError.ts +++ b/dlt-connector/src/graphql/model/TransactionError.ts @@ -1,4 +1,5 @@ import { ObjectType, Field } from 'type-graphql' + import { TransactionErrorType } from '../enum/TransactionErrorType' @ObjectType() diff --git a/dlt-connector/src/graphql/model/TransactionRecipe.ts b/dlt-connector/src/graphql/model/TransactionRecipe.ts new file mode 100644 index 000000000..263ccce4a --- /dev/null +++ b/dlt-connector/src/graphql/model/TransactionRecipe.ts @@ -0,0 +1,32 @@ +import { Transaction } from '@entity/Transaction' +import { Field, Int, ObjectType } from 'type-graphql' + +import { TransactionType } from '@/data/proto/3_3/enum/TransactionType' +import { LogError } from '@/server/LogError' +import { getEnumValue } from '@/utils/typeConverter' + +@ObjectType() +export class TransactionRecipe { + public constructor({ id, createdAt, type, community }: Transaction) { + const transactionType = getEnumValue(TransactionType, type) + if (!transactionType) { + throw new LogError('invalid transaction, type is missing') + } + this.id = id + this.createdAt = createdAt.toString() + this.type = transactionType.toString() + this.topic = community.iotaTopic + } + + @Field(() => Int) + id: number + + @Field(() => String) + createdAt: string + + @Field(() => String) + type: string + + @Field(() => String) + topic: string +} diff --git a/dlt-connector/src/graphql/model/TransactionResult.ts b/dlt-connector/src/graphql/model/TransactionResult.ts index 938c8bcf6..370c9827d 100644 --- a/dlt-connector/src/graphql/model/TransactionResult.ts +++ b/dlt-connector/src/graphql/model/TransactionResult.ts @@ -1,15 +1,17 @@ import { ObjectType, Field } from 'type-graphql' + import { TransactionError } from './TransactionError' +import { TransactionRecipe } from './TransactionRecipe' @ObjectType() export class TransactionResult { - constructor(content?: TransactionError | string) { + constructor(content?: TransactionError | TransactionRecipe) { this.succeed = true if (content instanceof TransactionError) { this.error = content this.succeed = false - } else if (typeof content === 'string') { - this.messageId = content + } else if (content instanceof TransactionRecipe) { + this.recipe = content } } @@ -18,8 +20,8 @@ export class TransactionResult { error?: TransactionError // if no error happend, the message id of the iota transaction - @Field(() => String, { nullable: true }) - messageId?: string + @Field(() => TransactionRecipe, { nullable: true }) + recipe?: TransactionRecipe @Field(() => Boolean) succeed: boolean diff --git a/dlt-connector/src/graphql/resolver/CommunityResolver.test.ts b/dlt-connector/src/graphql/resolver/CommunityResolver.test.ts index 7f1f3ea3c..0fa61cc48 100644 --- a/dlt-connector/src/graphql/resolver/CommunityResolver.test.ts +++ b/dlt-connector/src/graphql/resolver/CommunityResolver.test.ts @@ -1,34 +1,40 @@ import 'reflect-metadata' -import { ApolloServer } from '@apollo/server' -import { createApolloTestServer } from '@test/ApolloServerMock' import assert from 'assert' + +import { ApolloServer } from '@apollo/server' + +// must be imported before createApolloTestServer so that TestDB was created before createApolloTestServer imports repositories +// eslint-disable-next-line import/order import { TestDB } from '@test/TestDB' -import { TransactionResult } from '../model/TransactionResult' +import { TransactionResult } from '@model/TransactionResult' +import { createApolloTestServer } from '@test/ApolloServerMock' + +import { CONFIG } from '@/config' + +CONFIG.IOTA_HOME_COMMUNITY_SEED = 'aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899' + +const con = TestDB.instance + +jest.mock('@typeorm/DataSource', () => ({ + getDataSource: jest.fn(() => TestDB.instance.dbConnect), +})) let apolloTestServer: ApolloServer -jest.mock('@typeorm/DataSource', () => ({ - getDataSource: () => TestDB.instance.dbConnect, -})) - describe('graphql/resolver/CommunityResolver', () => { beforeAll(async () => { + await con.setupTestDB() apolloTestServer = await createApolloTestServer() }) + afterAll(async () => { + await con.teardownTestDB() + }) describe('tests with db', () => { - beforeAll(async () => { - await TestDB.instance.setupTestDB() - // apolloTestServer = await createApolloTestServer() - }) - - afterAll(async () => { - await TestDB.instance.teardownTestDB() - }) - it('test add foreign community', async () => { const response = await apolloTestServer.executeOperation({ - query: 'mutation ($input: CommunityDraft!) { addCommunity(data: $input) {succeed} }', + query: + 'mutation ($input: CommunityDraft!) { addCommunity(data: $input) {succeed, error {message}} }', variables: { input: { uuid: '3d813cbb-37fb-42ba-91df-831e1593ac29', @@ -45,7 +51,8 @@ describe('graphql/resolver/CommunityResolver', () => { it('test add home community', async () => { const response = await apolloTestServer.executeOperation({ - query: 'mutation ($input: CommunityDraft!) { addCommunity(data: $input) {succeed} }', + query: + 'mutation ($input: CommunityDraft!) { addCommunity(data: $input) {succeed, error {message}} }', variables: { input: { uuid: '3d823cad-37fb-41cd-91df-152e1593ac29', diff --git a/dlt-connector/src/graphql/resolver/CommunityResolver.ts b/dlt-connector/src/graphql/resolver/CommunityResolver.ts index d6f9f2d46..741de2e6d 100644 --- a/dlt-connector/src/graphql/resolver/CommunityResolver.ts +++ b/dlt-connector/src/graphql/resolver/CommunityResolver.ts @@ -1,47 +1,67 @@ -import { Resolver, Arg, Mutation } from 'type-graphql' +import { Resolver, Query, Arg, Mutation, Args } from 'type-graphql' +import { CommunityArg } from '@arg/CommunityArg' +import { TransactionErrorType } from '@enum/TransactionErrorType' import { CommunityDraft } from '@input/CommunityDraft' +import { Community } from '@model/Community' +import { TransactionError } from '@model/TransactionError' +import { TransactionResult } from '@model/TransactionResult' -import { TransactionResult } from '../model/TransactionResult' -import { TransactionError } from '../model/TransactionError' -import { create as createCommunity, isExist } from '@/controller/Community' -import { TransactionErrorType } from '../enum/TransactionErrorType' -import { logger } from '@/server/logger' +import { CommunityRepository } from '@/data/Community.repository' +import { AddCommunityContext } from '@/interactions/backendToDb/community/AddCommunity.context' +import { logger } from '@/logging/logger' +import { LogError } from '@/server/LogError' import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter' @Resolver() export class CommunityResolver { + @Query(() => Community) + async community(@Args() communityArg: CommunityArg): Promise { + logger.info('community', communityArg) + const result = await CommunityRepository.findByCommunityArg(communityArg) + if (result.length === 0) { + throw new LogError('cannot find community') + } else if (result.length === 1) { + return new Community(result[0]) + } else { + throw new LogError('find multiple communities') + } + } + + @Query(() => Boolean) + async isCommunityExist(@Args() communityArg: CommunityArg): Promise { + logger.info('isCommunity', communityArg) + return (await CommunityRepository.findByCommunityArg(communityArg)).length === 1 + } + + @Query(() => [Community]) + async communities(@Args() communityArg: CommunityArg): Promise { + logger.info('communities', communityArg) + const result = await CommunityRepository.findByCommunityArg(communityArg) + return result.map((communityEntity) => new Community(communityEntity)) + } + @Mutation(() => TransactionResult) async addCommunity( @Arg('data') communityDraft: CommunityDraft, ): Promise { + logger.info('addCommunity', communityDraft) + const topic = iotaTopicFromCommunityUUID(communityDraft.uuid) + // check if community was already written to db + if (await CommunityRepository.isExist(topic)) { + return new TransactionResult( + new TransactionError(TransactionErrorType.ALREADY_EXIST, 'community already exist!'), + ) + } + // prepare context for interaction + // shouldn't throw at all + // TODO: write tests to make sure that it doesn't throw + const addCommunityContext = new AddCommunityContext(communityDraft, topic) try { - const topic = iotaTopicFromCommunityUUID(communityDraft.uuid) - - // check if community was already written to db - if (await isExist(topic)) { - return new TransactionResult( - new TransactionError(TransactionErrorType.ALREADY_EXIST, 'community already exist!'), - ) - } - const community = createCommunity(communityDraft, topic) - - let result: TransactionResult - - if (!communityDraft.foreign) { - // TODO: CommunityRoot Transaction for blockchain - } - try { - await community.save() - result = new TransactionResult() - } catch (err) { - logger.error('error saving new community into db: %s', err) - result = new TransactionResult( - new TransactionError(TransactionErrorType.DB_ERROR, 'error saving community into db'), - ) - } - return result + // actually run interaction, create community, accounts for foreign community and transactionRecipe + await addCommunityContext.run() + return new TransactionResult() } catch (error) { if (error instanceof TransactionError) { return new TransactionResult(error) diff --git a/dlt-connector/src/graphql/resolver/TransactionsResolver.test.ts b/dlt-connector/src/graphql/resolver/TransactionsResolver.test.ts index 7c02a4306..6eb443e21 100644 --- a/dlt-connector/src/graphql/resolver/TransactionsResolver.test.ts +++ b/dlt-connector/src/graphql/resolver/TransactionsResolver.test.ts @@ -1,8 +1,28 @@ import 'reflect-metadata' -import { ApolloServer } from '@apollo/server' -import { createApolloTestServer } from '@test/ApolloServerMock' import assert from 'assert' -import { TransactionResult } from '../model/TransactionResult' + +import { ApolloServer } from '@apollo/server' + +// must be imported before createApolloTestServer so that TestDB was created before createApolloTestServer imports repositories +// eslint-disable-next-line import/order +import { TestDB } from '@test/TestDB' +import { AccountType } from '@enum/AccountType' +import { TransactionResult } from '@model/TransactionResult' +import { createApolloTestServer } from '@test/ApolloServerMock' + +import { CONFIG } from '@/config' +import { AccountFactory } from '@/data/Account.factory' +import { KeyPair } from '@/data/KeyPair' +import { Mnemonic } from '@/data/Mnemonic' +import { UserFactory } from '@/data/User.factory' +import { UserLogic } from '@/data/User.logic' +import { AddCommunityContext } from '@/interactions/backendToDb/community/AddCommunity.context' +import { getEnumValue } from '@/utils/typeConverter' + +import { InputTransactionType } from '../enum/InputTransactionType' +import { CommunityDraft } from '../input/CommunityDraft' +import { UserAccountDraft } from '../input/UserAccountDraft' +import { UserIdentifier } from '../input/UserIdentifier' let apolloTestServer: ApolloServer @@ -14,63 +34,88 @@ jest.mock('@/client/IotaClient', () => { } }) +jest.mock('@typeorm/DataSource', () => ({ + getDataSource: jest.fn(() => TestDB.instance.dbConnect), +})) + +CONFIG.IOTA_HOME_COMMUNITY_SEED = 'aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899' +const communityUUID = '3d813cbb-37fb-42ba-91df-831e1593ac29' +const communityKeyPair = new KeyPair(new Mnemonic(CONFIG.IOTA_HOME_COMMUNITY_SEED)) + +const createUserStoreAccount = async (uuid: string): Promise => { + const userAccountDraft = new UserAccountDraft() + userAccountDraft.accountType = AccountType.COMMUNITY_HUMAN + userAccountDraft.createdAt = new Date().toString() + userAccountDraft.user = new UserIdentifier() + userAccountDraft.user.uuid = uuid + userAccountDraft.user.communityUuid = communityUUID + const user = UserFactory.create(userAccountDraft, communityKeyPair) + const userLogic = new UserLogic(user) + const account = AccountFactory.createAccountFromUserAccountDraft( + userAccountDraft, + userLogic.calculateKeyPair(communityKeyPair), + ) + account.user = user + // user is set to cascade: ['insert'] will be saved together with account + await account.save() + return userAccountDraft.user +} + describe('Transaction Resolver Test', () => { + let user: UserIdentifier + let linkedUser: UserIdentifier beforeAll(async () => { + await TestDB.instance.setupTestDB() apolloTestServer = await createApolloTestServer() + + const communityDraft = new CommunityDraft() + communityDraft.uuid = communityUUID + communityDraft.foreign = false + communityDraft.createdAt = new Date().toString() + const addCommunityContext = new AddCommunityContext(communityDraft) + await addCommunityContext.run() + user = await createUserStoreAccount('0ec72b74-48c2-446f-91ce-31ad7d9f4d65') + linkedUser = await createUserStoreAccount('ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe') }) - it('test version query', async () => { - const response = await apolloTestServer.executeOperation({ - query: '{ version }', - }) - // Note the use of Node's assert rather than Jest's expect; if using - // TypeScript, `assert`` will appropriately narrow the type of `body` - // and `expect` will not. - // Source: https://www.apollographql.com/docs/apollo-server/testing/testing - assert(response.body.kind === 'single') - expect(response.body.singleResult.errors).toBeUndefined() - expect(response.body.singleResult.data?.version).toBe('0.1') + + afterAll(async () => { + await TestDB.instance.teardownTestDB() }) + it('test mocked sendTransaction', async () => { const response = await apolloTestServer.executeOperation({ query: - 'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, messageId} }', + 'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {succeed, recipe { id, topic }} }', variables: { input: { - senderUser: { - uuid: '0ec72b74-48c2-446f-91ce-31ad7d9f4d65', - }, - recipientUser: { - uuid: 'ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe', - }, - type: 'SEND', + user, + linkedUser, + type: getEnumValue(InputTransactionType, InputTransactionType.SEND), amount: '10', createdAt: '2012-04-17T17:12:00Z', + backendTransactionId: 1, }, }, }) assert(response.body.kind === 'single') expect(response.body.singleResult.errors).toBeUndefined() const transactionResult = response.body.singleResult.data?.sendTransaction as TransactionResult - expect(transactionResult.messageId).toBe( - '5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710', - ) + expect(transactionResult.recipe).toBeDefined() + expect(transactionResult.succeed).toBe(true) }) it('test mocked sendTransaction invalid transactionType ', async () => { const response = await apolloTestServer.executeOperation({ query: - 'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, messageId} }', + 'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, succeed} }', variables: { input: { - senderUser: { - uuid: '0ec72b74-48c2-446f-91ce-31ad7d9f4d65', - }, - recipientUser: { - uuid: 'ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe', - }, + user, + linkedUser, type: 'INVALID', amount: '10', createdAt: '2012-04-17T17:12:00Z', + backendTransactionId: 1, }, }, }) @@ -79,7 +124,7 @@ describe('Transaction Resolver Test', () => { errors: [ { message: - 'Variable "$input" got invalid value "INVALID" at "input.type"; Value "INVALID" does not exist in "TransactionType" enum.', + 'Variable "$input" got invalid value "INVALID" at "input.type"; Value "INVALID" does not exist in "InputTransactionType" enum.', }, ], }) @@ -88,18 +133,15 @@ describe('Transaction Resolver Test', () => { it('test mocked sendTransaction invalid amount ', async () => { const response = await apolloTestServer.executeOperation({ query: - 'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, messageId} }', + 'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, succeed} }', variables: { input: { - senderUser: { - uuid: '0ec72b74-48c2-446f-91ce-31ad7d9f4d65', - }, - recipientUser: { - uuid: 'ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe', - }, - type: 'SEND', + user, + linkedUser, + type: getEnumValue(InputTransactionType, InputTransactionType.SEND), amount: 'no number', createdAt: '2012-04-17T17:12:00Z', + backendTransactionId: 1, }, }, }) @@ -117,18 +159,15 @@ describe('Transaction Resolver Test', () => { it('test mocked sendTransaction invalid created date ', async () => { const response = await apolloTestServer.executeOperation({ query: - 'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, messageId} }', + 'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, succeed} }', variables: { input: { - senderUser: { - uuid: '0ec72b74-48c2-446f-91ce-31ad7d9f4d65', - }, - recipientUser: { - uuid: 'ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe', - }, - type: 'SEND', + user, + linkedUser, + type: getEnumValue(InputTransactionType, InputTransactionType.SEND), amount: '10', createdAt: 'not valid', + backendTransactionId: 1, }, }, }) @@ -156,18 +195,15 @@ describe('Transaction Resolver Test', () => { it('test mocked sendTransaction missing creationDate for contribution', async () => { const response = await apolloTestServer.executeOperation({ query: - 'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, messageId} }', + 'mutation ($input: TransactionDraft!) { sendTransaction(data: $input) {error {type, message}, succeed} }', variables: { input: { - senderUser: { - uuid: '0ec72b74-48c2-446f-91ce-31ad7d9f4d65', - }, - recipientUser: { - uuid: 'ddc8258e-fcb5-4e48-8d1d-3a07ec371dbe', - }, - type: 'CREATION', + user, + linkedUser, + type: getEnumValue(InputTransactionType, InputTransactionType.CREATION), amount: '10', createdAt: '2012-04-17T17:12:00Z', + backendTransactionId: 1, }, }, }) diff --git a/dlt-connector/src/graphql/resolver/TransactionsResolver.ts b/dlt-connector/src/graphql/resolver/TransactionsResolver.ts index 282eb11cd..6a5017fb1 100755 --- a/dlt-connector/src/graphql/resolver/TransactionsResolver.ts +++ b/dlt-connector/src/graphql/resolver/TransactionsResolver.ts @@ -1,40 +1,56 @@ -import { Resolver, Query, Arg, Mutation } from 'type-graphql' +import { Resolver, Arg, Mutation } from 'type-graphql' import { TransactionDraft } from '@input/TransactionDraft' -import { create as createTransactionBody } from '@controller/TransactionBody' -import { create as createGradidoTransaction } from '@controller/GradidoTransaction' +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 { LogError } from '@/server/LogError' -import { sendMessage as iotaSendMessage } from '@/client/IotaClient' -import { GradidoTransaction } from '@/proto/3_3/GradidoTransaction' -import { TransactionResult } from '../model/TransactionResult' import { TransactionError } from '../model/TransactionError' +import { TransactionRecipe } from '../model/TransactionRecipe' +import { TransactionResult } from '../model/TransactionResult' @Resolver() export class TransactionResolver { - // Why a dummy function? - // to prevent this error by start: - // GeneratingSchemaError: Some errors occurred while generating GraphQL schema: - // Type Query must define one or more fields. - // it seems that at least one query must be defined - // https://github.com/ardatan/graphql-tools/issues/764 - @Query(() => String) - version(): string { - return '0.1' - } - @Mutation(() => TransactionResult) async sendTransaction( @Arg('data') - transaction: TransactionDraft, + transactionDraft: TransactionDraft, ): Promise { + const createTransactionRecipeContext = new CreateTransactionRecipeContext(transactionDraft) try { - const body = createTransactionBody(transaction) - const message = createGradidoTransaction(body) - const messageBuffer = GradidoTransaction.encode(message).finish() - const resultMessage = await iotaSendMessage(messageBuffer) - return new TransactionResult(resultMessage.messageId) - } catch (error) { + await createTransactionRecipeContext.run() + const transactionRecipe = createTransactionRecipeContext.getTransactionRecipe() + // check if a transaction with this signature already exist + const existingRecipe = await TransactionRepository.findBySignature( + transactionRecipe.signature, + ) + if (existingRecipe) { + // transaction recipe with this signature already exist, we need only to store the backendTransaction + if (transactionRecipe.backendTransactions.length !== 1) { + throw new LogError('unexpected backend transaction count', { + count: transactionRecipe.backendTransactions.length, + transactionId: transactionRecipe.id, + }) + } + const backendTransaction = transactionRecipe.backendTransactions[0] + backendTransaction.transactionId = transactionRecipe.id + logger.debug( + 'store backendTransaction', + new BackendTransactionLoggingView(backendTransaction), + ) + await backendTransaction.save() + } else { + logger.debug('store transaction recipe', new TransactionLoggingView(transactionRecipe)) + // we can store the transaction and with that automatic the backend transaction + await transactionRecipe.save() + } + return new TransactionResult(new TransactionRecipe(transactionRecipe)) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { if (error instanceof TransactionError) { return new TransactionResult(error) } else { diff --git a/dlt-connector/src/graphql/schema.ts b/dlt-connector/src/graphql/schema.ts index fc9c26919..bbd61c63f 100755 --- a/dlt-connector/src/graphql/schema.ts +++ b/dlt-connector/src/graphql/schema.ts @@ -2,9 +2,9 @@ import { Decimal } from 'decimal.js-light' import { GraphQLSchema } from 'graphql' import { buildSchema } from 'type-graphql' -import { DecimalScalar } from './scalar/Decimal' -import { TransactionResolver } from './resolver/TransactionsResolver' import { CommunityResolver } from './resolver/CommunityResolver' +import { TransactionResolver } from './resolver/TransactionsResolver' +import { DecimalScalar } from './scalar/Decimal' export const schema = async (): Promise => { return buildSchema({ diff --git a/dlt-connector/src/index.ts b/dlt-connector/src/index.ts index 5284d9b1e..c72978b35 100644 --- a/dlt-connector/src/index.ts +++ b/dlt-connector/src/index.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { CONFIG } from '@/config' + import createServer from './server/createServer' async function main() { diff --git a/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.ts b/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.ts new file mode 100644 index 000000000..bc8f90c32 --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/community/AddCommunity.context.ts @@ -0,0 +1,31 @@ +import { CommunityDraft } from '@/graphql/input/CommunityDraft' +import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter' + +import { CommunityRole } from './Community.role' +import { ForeignCommunityRole } from './ForeignCommunity.role' +import { HomeCommunityRole } from './HomeCommunity.role' + +/** + * @DCI-Context + * Context for adding community to DB + * using roles to distinct between foreign and home communities + */ +export class AddCommunityContext { + private communityRole: CommunityRole + private iotaTopic: string + public constructor(private communityDraft: CommunityDraft, iotaTopic?: string) { + if (!iotaTopic) { + this.iotaTopic = iotaTopicFromCommunityUUID(this.communityDraft.uuid) + } else { + this.iotaTopic = iotaTopic + } + this.communityRole = communityDraft.foreign + ? new ForeignCommunityRole() + : new HomeCommunityRole() + } + + public async run(): Promise { + await this.communityRole.create(this.communityDraft, this.iotaTopic) + await this.communityRole.store() + } +} diff --git a/dlt-connector/src/interactions/backendToDb/community/Community.role.ts b/dlt-connector/src/interactions/backendToDb/community/Community.role.ts new file mode 100644 index 000000000..2b1514ef2 --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/community/Community.role.ts @@ -0,0 +1,31 @@ +import { Community } from '@entity/Community' + +import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' +import { CommunityDraft } from '@/graphql/input/CommunityDraft' +import { TransactionError } from '@/graphql/model/TransactionError' +import { CommunityLoggingView } from '@/logging/CommunityLogging.view' +import { logger } from '@/logging/logger' + +export abstract class CommunityRole { + protected self: Community + public constructor() { + this.self = Community.create() + } + + public async create(communityDraft: CommunityDraft, topic: string): Promise { + this.self.iotaTopic = topic + this.self.createdAt = new Date(communityDraft.createdAt) + this.self.foreign = communityDraft.foreign + } + + public async store(): Promise { + try { + const community = await this.self.save() + logger.debug('store community', new CommunityLoggingView(community)) + return community + } catch (error) { + logger.error('error saving new community into db: %s', error) + throw new TransactionError(TransactionErrorType.DB_ERROR, 'error saving community into db') + } + } +} diff --git a/dlt-connector/src/interactions/backendToDb/community/ForeignCommunity.role.ts b/dlt-connector/src/interactions/backendToDb/community/ForeignCommunity.role.ts new file mode 100644 index 000000000..cf93deaa5 --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/community/ForeignCommunity.role.ts @@ -0,0 +1,4 @@ +import { CommunityRole } from './Community.role' + +// same as base class +export class ForeignCommunityRole extends CommunityRole {} diff --git a/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts b/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts new file mode 100644 index 000000000..7a4798368 --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts @@ -0,0 +1,53 @@ +import { Community } from '@entity/Community' +import { Transaction } from '@entity/Transaction' + +import { CONFIG } from '@/config' +import { AccountFactory } from '@/data/Account.factory' +import { KeyPair } from '@/data/KeyPair' +import { Mnemonic } from '@/data/Mnemonic' +import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' +import { CommunityDraft } from '@/graphql/input/CommunityDraft' +import { TransactionError } from '@/graphql/model/TransactionError' +import { CommunityLoggingView } from '@/logging/CommunityLogging.view' +import { logger } from '@/logging/logger' +import { getDataSource } from '@/typeorm/DataSource' + +import { CreateTransactionRecipeContext } from '../transaction/CreateTransationRecipe.context' + +import { CommunityRole } from './Community.role' + +export class HomeCommunityRole extends CommunityRole { + private transactionRecipe: Transaction + + 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)) + keyPair.fillInCommunityKeys(this.self) + + // create auf account and gmw account + this.self.aufAccount = AccountFactory.createAufAccount(keyPair, this.self.createdAt) + this.self.gmwAccount = AccountFactory.createGmwAccount(keyPair, this.self.createdAt) + + const transactionRecipeContext = new CreateTransactionRecipeContext(communityDraft, this.self) + await transactionRecipeContext.run() + this.transactionRecipe = transactionRecipeContext.getTransactionRecipe() + } + + public async store(): Promise { + try { + return 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 + }) + } catch (error) { + logger.error('error saving home community into db: %s', error) + throw new TransactionError( + TransactionErrorType.DB_ERROR, + 'error saving home community into db', + ) + } + } +} diff --git a/dlt-connector/src/interactions/backendToDb/transaction/AbstractTransaction.role.ts b/dlt-connector/src/interactions/backendToDb/transaction/AbstractTransaction.role.ts new file mode 100644 index 000000000..62fcf90de --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/transaction/AbstractTransaction.role.ts @@ -0,0 +1,62 @@ +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' + +export abstract class AbstractTransactionRole { + // eslint-disable-next-line no-useless-constructor + public constructor(protected self: TransactionDraft) {} + + abstract getSigningUser(): UserIdentifier + abstract getRecipientUser(): UserIdentifier + abstract getCrossGroupType(): CrossGroupType + + public isCrossGroupTransaction(): boolean { + return ( + this.self.user.communityUuid !== this.self.linkedUser.communityUuid && + this.self.linkedUser.communityUuid !== '' + ) + } + + /** + * otherGroup is the group/community on which this part of the transaction isn't stored + * Alice from 'gdd1' Send 10 GDD to Bob in 'gdd2' + * OUTBOUND came from sender, stored on sender community blockchain + * OUTBOUND: stored on 'gdd1', otherGroup: 'gdd2' + * INBOUND: goes to receiver, stored on receiver community blockchain + * INBOUND: stored on 'gdd2', otherGroup: 'gdd1' + * @returns + */ + public getOtherGroup(): string { + let user: UserIdentifier + const type = this.getCrossGroupType() + switch (type) { + case CrossGroupType.LOCAL: + return '' + case CrossGroupType.INBOUND: + user = this.getSigningUser() + if (!user.communityUuid) { + throw new TransactionError( + TransactionErrorType.MISSING_PARAMETER, + 'missing sender/signing user community id for cross group transaction', + ) + } + return user.communityUuid + case CrossGroupType.OUTBOUND: + user = this.getRecipientUser() + if (!user.communityUuid) { + throw new TransactionError( + TransactionErrorType.MISSING_PARAMETER, + 'missing recipient user community id for cross group transaction', + ) + } + return user.communityUuid + default: + throw new TransactionError( + TransactionErrorType.NOT_IMPLEMENTED_YET, + `type not implemented yet ${type}`, + ) + } + } +} diff --git a/dlt-connector/src/interactions/backendToDb/transaction/CommunityRootTransaction.role.ts b/dlt-connector/src/interactions/backendToDb/transaction/CommunityRootTransaction.role.ts new file mode 100644 index 000000000..75b885f2f --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/transaction/CommunityRootTransaction.role.ts @@ -0,0 +1,25 @@ +import { Community } from '@entity/Community' + +import { KeyPair } from '@/data/KeyPair' +import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder' +import { CommunityDraft } from '@/graphql/input/CommunityDraft' + +import { TransactionRecipeRole } from './TransactionRecipe.role' + +export class CommunityRootTransactionRole extends TransactionRecipeRole { + public createFromCommunityRoot( + communityDraft: CommunityDraft, + community: Community, + ): CommunityRootTransactionRole { + // create proto transaction body + const transactionBody = new TransactionBodyBuilder() + .fromCommunityDraft(communityDraft, community) + .build() + // build transaction entity + this.transactionBuilder.fromTransactionBody(transactionBody).setCommunity(community) + const transaction = this.transactionBuilder.getTransaction() + // sign + this.transactionBuilder.setSignature(new KeyPair(community).sign(transaction.bodyBytes)) + return this + } +} diff --git a/dlt-connector/src/interactions/backendToDb/transaction/CreateTransationRecipe.context.ts b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransationRecipe.context.ts new file mode 100644 index 000000000..8fa3dc443 --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/transaction/CreateTransationRecipe.context.ts @@ -0,0 +1,67 @@ +import { Community } from '@entity/Community' +import { Transaction } from '@entity/Transaction' + +import { InputTransactionType } from '@/graphql/enum/InputTransactionType' +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 { AbstractTransactionRole } from './AbstractTransaction.role' +import { CommunityRootTransactionRole } from './CommunityRootTransaction.role' +import { CreationTransactionRole } from './CreationTransaction.role' +import { ReceiveTransactionRole } from './ReceiveTransaction.role' +import { SendTransactionRole } from './SendTransaction.role' +import { TransactionRecipeRole } from './TransactionRecipe.role' + +/** + * @DCI-Context + * Context for create and add Transaction Recipe to DB + */ + +export class CreateTransactionRecipeContext { + private transactionRecipeRole: TransactionRecipeRole + // eslint-disable-next-line no-useless-constructor + public constructor( + private draft: CommunityDraft | TransactionDraft, + private community?: Community, + ) {} + + public getTransactionRecipe(): Transaction { + return this.transactionRecipeRole.getTransaction() + } + + /** + * @returns true if a transaction recipe was created and false if it wasn't necessary + */ + public async run(): Promise { + if (this.draft instanceof TransactionDraft) { + this.transactionRecipeRole = new TransactionRecipeRole() + // contain logic for translation from backend to dlt-connector format + let transactionTypeRole: AbstractTransactionRole + switch (this.draft.type) { + case InputTransactionType.CREATION: + transactionTypeRole = new CreationTransactionRole(this.draft) + break + case InputTransactionType.SEND: + transactionTypeRole = new SendTransactionRole(this.draft) + break + case InputTransactionType.RECEIVE: + transactionTypeRole = new ReceiveTransactionRole(this.draft) + break + } + await this.transactionRecipeRole.create(this.draft, transactionTypeRole) + return true + } else if (this.draft instanceof CommunityDraft) { + if (!this.community) { + throw new TransactionError(TransactionErrorType.MISSING_PARAMETER, 'community was not set') + } + this.transactionRecipeRole = new CommunityRootTransactionRole().createFromCommunityRoot( + this.draft, + this.community, + ) + return true + } + return false + } +} diff --git a/dlt-connector/src/interactions/backendToDb/transaction/CreationTransaction.role.ts b/dlt-connector/src/interactions/backendToDb/transaction/CreationTransaction.role.ts new file mode 100644 index 000000000..7b82f8805 --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/transaction/CreationTransaction.role.ts @@ -0,0 +1,18 @@ +import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType' +import { UserIdentifier } from '@/graphql/input/UserIdentifier' + +import { AbstractTransactionRole } from './AbstractTransaction.role' + +export class CreationTransactionRole extends AbstractTransactionRole { + public getSigningUser(): UserIdentifier { + return this.self.linkedUser + } + + public getRecipientUser(): UserIdentifier { + return this.self.user + } + + public getCrossGroupType(): CrossGroupType { + return CrossGroupType.LOCAL + } +} diff --git a/dlt-connector/src/interactions/backendToDb/transaction/ReceiveTransaction.role.ts b/dlt-connector/src/interactions/backendToDb/transaction/ReceiveTransaction.role.ts new file mode 100644 index 000000000..bf7c69f0e --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/transaction/ReceiveTransaction.role.ts @@ -0,0 +1,21 @@ +import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType' +import { UserIdentifier } from '@/graphql/input/UserIdentifier' + +import { AbstractTransactionRole } from './AbstractTransaction.role' + +export class ReceiveTransactionRole extends AbstractTransactionRole { + public getSigningUser(): UserIdentifier { + return this.self.linkedUser + } + + public getRecipientUser(): UserIdentifier { + return this.self.user + } + + public getCrossGroupType(): CrossGroupType { + if (this.isCrossGroupTransaction()) { + return CrossGroupType.INBOUND + } + return CrossGroupType.LOCAL + } +} diff --git a/dlt-connector/src/interactions/backendToDb/transaction/SendTransaction.role.ts b/dlt-connector/src/interactions/backendToDb/transaction/SendTransaction.role.ts new file mode 100644 index 000000000..927efdc24 --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/transaction/SendTransaction.role.ts @@ -0,0 +1,21 @@ +import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType' +import { UserIdentifier } from '@/graphql/input/UserIdentifier' + +import { AbstractTransactionRole } from './AbstractTransaction.role' + +export class SendTransactionRole extends AbstractTransactionRole { + public getSigningUser(): UserIdentifier { + return this.self.user + } + + public getRecipientUser(): UserIdentifier { + return this.self.linkedUser + } + + public getCrossGroupType(): CrossGroupType { + if (this.isCrossGroupTransaction()) { + return CrossGroupType.OUTBOUND + } + return CrossGroupType.LOCAL + } +} diff --git a/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts b/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts new file mode 100644 index 000000000..d36aa98cc --- /dev/null +++ b/dlt-connector/src/interactions/backendToDb/transaction/TransactionRecipe.role.ts @@ -0,0 +1,70 @@ +import { Transaction } from '@entity/Transaction' + +import { KeyPair } from '@/data/KeyPair' +import { TransactionBodyBuilder } from '@/data/proto/TransactionBody.builder' +import { TransactionBuilder } from '@/data/Transaction.builder' +import { UserRepository } from '@/data/User.repository' +import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' +import { TransactionDraft } from '@/graphql/input/TransactionDraft' +import { TransactionError } from '@/graphql/model/TransactionError' + +import { AbstractTransactionRole } from './AbstractTransaction.role' + +export class TransactionRecipeRole { + protected transactionBuilder: TransactionBuilder + + public constructor() { + this.transactionBuilder = new TransactionBuilder() + } + + public async create( + transactionDraft: TransactionDraft, + transactionTypeRole: AbstractTransactionRole, + ): Promise { + const signingUser = transactionTypeRole.getSigningUser() + const recipientUser = transactionTypeRole.getRecipientUser() + + // loading signing and recipient account + // TODO: look for ways to use only one db call for both + const signingAccount = await UserRepository.findAccountByUserIdentifier(signingUser) + if (!signingAccount) { + throw new TransactionError( + TransactionErrorType.NOT_FOUND, + "couldn't found sender user account in db", + ) + } + const recipientAccount = await UserRepository.findAccountByUserIdentifier(recipientUser) + if (!recipientAccount) { + throw new TransactionError( + TransactionErrorType.NOT_FOUND, + "couldn't found recipient user account in db", + ) + } + // create proto transaction body + const transactionBodyBuilder = new TransactionBodyBuilder() + .setSigningAccount(signingAccount) + .setRecipientAccount(recipientAccount) + .fromTransactionDraft(transactionDraft) + .setCrossGroupType(transactionTypeRole.getCrossGroupType()) + .setOtherGroup(transactionTypeRole.getOtherGroup()) + + // build transaction entity + this.transactionBuilder + .fromTransactionBodyBuilder(transactionBodyBuilder) + .addBackendTransaction(transactionDraft) + await this.transactionBuilder.setSenderCommunityFromSenderUser(signingUser) + if (recipientUser.communityUuid !== signingUser.communityUuid) { + await this.transactionBuilder.setOtherCommunityFromRecipientUser(recipientUser) + } + const transaction = this.transactionBuilder.getTransaction() + // sign + this.transactionBuilder.setSignature( + new KeyPair(this.transactionBuilder.getCommunity()).sign(transaction.bodyBytes), + ) + return this + } + + public getTransaction(): Transaction { + return this.transactionBuilder.getTransaction() + } +} diff --git a/dlt-connector/src/logging/AbstractLogging.view.ts b/dlt-connector/src/logging/AbstractLogging.view.ts new file mode 100644 index 000000000..ad52e6530 --- /dev/null +++ b/dlt-connector/src/logging/AbstractLogging.view.ts @@ -0,0 +1,49 @@ +import util from 'util' + +import { Decimal } from 'decimal.js-light' + +import { Timestamp } from '@/data/proto/3_3/Timestamp' +import { TimestampSeconds } from '@/data/proto/3_3/TimestampSeconds' +import { timestampSecondsToDate, timestampToDate } from '@/utils/typeConverter' + +export abstract class AbstractLoggingView { + protected bufferStringFormat: BufferEncoding = 'hex' + + // This function gets called automatically when JSON.stringify() is called on this class instance + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public abstract toJSON(): any + public toString(): string { + return JSON.stringify(this.toJSON(), null, 2) + } + + // called form console.log or log4js logging functions + [util.inspect.custom](): string { + return this.toString() + } + + protected dateToString(date: Date | undefined | null): string | undefined { + if (date) { + return date.toISOString() + } + return undefined + } + + protected decimalToString(number: Decimal | undefined | null): string | undefined { + if (number) { + return number.toString() + } + return undefined + } + + protected timestampSecondsToDateString(timestamp: TimestampSeconds): string | undefined { + if (timestamp && timestamp.seconds) { + return timestampSecondsToDate(timestamp).toISOString() + } + } + + protected timestampToDateString(timestamp: Timestamp): string | undefined { + if (timestamp && (timestamp.seconds || timestamp.nanoSeconds)) { + return timestampToDate(timestamp).toISOString() + } + } +} diff --git a/dlt-connector/src/logging/AccountLogging.view.ts b/dlt-connector/src/logging/AccountLogging.view.ts new file mode 100644 index 000000000..76ff7b891 --- /dev/null +++ b/dlt-connector/src/logging/AccountLogging.view.ts @@ -0,0 +1,29 @@ +import { Account } from '@entity/Account' + +import { AddressType } from '@/data/proto/3_3/enum/AddressType' +import { getEnumValue } from '@/utils/typeConverter' + +import { AbstractLoggingView } from './AbstractLogging.view' +import { UserLoggingView } from './UserLogging.view' + +export class AccountLoggingView extends AbstractLoggingView { + public constructor(private account: Account) { + super() + } + + public toJSON() { + return { + 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), + type: getEnumValue(AddressType, this.account.type), + createdAt: this.dateToString(this.account.createdAt), + confirmedAt: this.dateToString(this.account.confirmedAt), + balanceOnConfirmation: this.decimalToString(this.account.balanceOnConfirmation), + balanceConfirmedAt: this.dateToString(this.account.balanceConfirmedAt), + balanceOnCreation: this.decimalToString(this.account.balanceOnCreation), + balanceCreatedAt: this.dateToString(this.account.balanceCreatedAt), + } + } +} diff --git a/dlt-connector/src/logging/BackendTransactionLogging.view.ts b/dlt-connector/src/logging/BackendTransactionLogging.view.ts new file mode 100644 index 000000000..d21c765aa --- /dev/null +++ b/dlt-connector/src/logging/BackendTransactionLogging.view.ts @@ -0,0 +1,30 @@ +import { BackendTransaction } from '@entity/BackendTransaction' + +import { InputTransactionType } from '@/graphql/enum/InputTransactionType' +import { getEnumValue } from '@/utils/typeConverter' + +import { AbstractLoggingView } from './AbstractLogging.view' +import { TransactionLoggingView } from './TransactionLogging.view' + +export class BackendTransactionLoggingView extends AbstractLoggingView { + public constructor(private self: BackendTransaction) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(showTransaction = true): any { + return { + id: this.self.id, + backendTransactionId: this.self.backendTransactionId, + transaction: + showTransaction && this.self.transaction + ? new TransactionLoggingView(this.self.transaction).toJSON(false) + : undefined, + type: getEnumValue(InputTransactionType, this.self.typeId), + balance: this.decimalToString(this.self.balance), + createdAt: this.dateToString(this.self.createdAt), + confirmedAt: this.dateToString(this.self.confirmedAt), + verifiedOnBackend: this.self.verifiedOnBackend, + } + } +} diff --git a/dlt-connector/src/logging/CommunityLogging.view.ts b/dlt-connector/src/logging/CommunityLogging.view.ts new file mode 100644 index 000000000..22f0a4597 --- /dev/null +++ b/dlt-connector/src/logging/CommunityLogging.view.ts @@ -0,0 +1,24 @@ +import { Community } from '@entity/Community' + +import { AbstractLoggingView } from './AbstractLogging.view' +import { AccountLoggingView } from './AccountLogging.view' + +export class CommunityLoggingView extends AbstractLoggingView { + public constructor(private self: Community) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + id: this.self.id, + iotaTopic: this.self.iotaTopic, + foreign: this.self.foreign, + publicKey: this.self.rootPubkey?.toString(this.bufferStringFormat), + createdAt: this.dateToString(this.self.createdAt), + confirmedAt: this.dateToString(this.self.confirmedAt), + aufAccount: this.self.aufAccount ? new AccountLoggingView(this.self.aufAccount) : undefined, + gmwAccount: this.self.gmwAccount ? new AccountLoggingView(this.self.gmwAccount) : undefined, + } + } +} diff --git a/dlt-connector/src/logging/CommunityRootLogging.view.ts b/dlt-connector/src/logging/CommunityRootLogging.view.ts new file mode 100644 index 000000000..ba2869755 --- /dev/null +++ b/dlt-connector/src/logging/CommunityRootLogging.view.ts @@ -0,0 +1,18 @@ +import { CommunityRoot } from '@/data/proto/3_3/CommunityRoot' + +import { AbstractLoggingView } from './AbstractLogging.view' + +export class CommunityRootLoggingView extends AbstractLoggingView { + public constructor(private self: CommunityRoot) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + rootPubkey: Buffer.from(this.self.rootPubkey).toString(this.bufferStringFormat), + gmwPubkey: Buffer.from(this.self.gmwPubkey).toString(this.bufferStringFormat), + aufPubkey: Buffer.from(this.self.aufPubkey).toString(this.bufferStringFormat), + } + } +} diff --git a/dlt-connector/src/logging/ConfirmedTransactionLogging.view.ts b/dlt-connector/src/logging/ConfirmedTransactionLogging.view.ts new file mode 100644 index 000000000..8e894a35a --- /dev/null +++ b/dlt-connector/src/logging/ConfirmedTransactionLogging.view.ts @@ -0,0 +1,24 @@ +import { ConfirmedTransaction } from '@/data/proto/3_3/ConfirmedTransaction' +import { timestampSecondsToDate } from '@/utils/typeConverter' + +import { AbstractLoggingView } from './AbstractLogging.view' +import { GradidoTransactionLoggingView } from './GradidoTransactionLogging.view' + +export class ConfirmedTransactionLoggingView extends AbstractLoggingView { + public constructor(private self: ConfirmedTransaction) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + id: this.self.id.toString(), + transaction: new GradidoTransactionLoggingView(this.self.transaction).toJSON(), + confirmedAt: this.dateToString(timestampSecondsToDate(this.self.confirmedAt)), + versionNumber: this.self.versionNumber, + runningHash: Buffer.from(this.self.runningHash).toString(this.bufferStringFormat), + messageId: Buffer.from(this.self.messageId).toString(this.bufferStringFormat), + accountBalance: this.self.accountBalance, + } + } +} diff --git a/dlt-connector/src/logging/GradidoCreationLogging.view.ts b/dlt-connector/src/logging/GradidoCreationLogging.view.ts new file mode 100644 index 000000000..43e14b887 --- /dev/null +++ b/dlt-connector/src/logging/GradidoCreationLogging.view.ts @@ -0,0 +1,18 @@ +import { GradidoCreation } from '@/data/proto/3_3/GradidoCreation' + +import { AbstractLoggingView } from './AbstractLogging.view' +import { TransferAmountLoggingView } from './TransferAmountLogging.view' + +export class GradidoCreationLoggingView extends AbstractLoggingView { + public constructor(private self: GradidoCreation) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + recipient: new TransferAmountLoggingView(this.self.recipient).toJSON(), + targetDate: this.timestampSecondsToDateString(this.self.targetDate), + } + } +} diff --git a/dlt-connector/src/logging/GradidoDeferredTransferLogging.view.ts b/dlt-connector/src/logging/GradidoDeferredTransferLogging.view.ts new file mode 100644 index 000000000..89a1f1a29 --- /dev/null +++ b/dlt-connector/src/logging/GradidoDeferredTransferLogging.view.ts @@ -0,0 +1,18 @@ +import { GradidoDeferredTransfer } from '@/data/proto/3_3/GradidoDeferredTransfer' + +import { AbstractLoggingView } from './AbstractLogging.view' +import { GradidoTransferLoggingView } from './GradidoTransferLogging.view' + +export class GradidoDeferredTransferLoggingView extends AbstractLoggingView { + public constructor(private self: GradidoDeferredTransfer) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + ...new GradidoTransferLoggingView(this.self.transfer).toJSON(), + ...{ timeout: this.timestampSecondsToDateString(this.self.timeout) }, + } + } +} diff --git a/dlt-connector/src/logging/GradidoTransactionLogging.view.ts b/dlt-connector/src/logging/GradidoTransactionLogging.view.ts new file mode 100644 index 000000000..f23c0b05e --- /dev/null +++ b/dlt-connector/src/logging/GradidoTransactionLogging.view.ts @@ -0,0 +1,29 @@ +import { GradidoTransaction } from '@/data/proto/3_3/GradidoTransaction' +import { TransactionBody } from '@/data/proto/3_3/TransactionBody' + +import { AbstractLoggingView } from './AbstractLogging.view' +import { SignatureMapLoggingView } from './SignatureMapLogging.view' +import { TransactionBodyLoggingView } from './TransactionBodyLogging.view' + +export class GradidoTransactionLoggingView extends AbstractLoggingView { + public constructor(private self: GradidoTransaction) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + let transactionBody: TransactionBody | null | unknown = null + try { + transactionBody = new TransactionBodyLoggingView(this.self.getTransactionBody()) + } catch (e) { + transactionBody = e + } + return { + sigMap: new SignatureMapLoggingView(this.self.sigMap).toJSON(), + bodyBytes: transactionBody, + parentMessageId: this.self.parentMessageId + ? Buffer.from(this.self.parentMessageId).toString(this.bufferStringFormat) + : undefined, + } + } +} diff --git a/dlt-connector/src/logging/GradidoTransferLogging.view.ts b/dlt-connector/src/logging/GradidoTransferLogging.view.ts new file mode 100644 index 000000000..84b5fe604 --- /dev/null +++ b/dlt-connector/src/logging/GradidoTransferLogging.view.ts @@ -0,0 +1,18 @@ +import { GradidoTransfer } from '@/data/proto/3_3/GradidoTransfer' + +import { AbstractLoggingView } from './AbstractLogging.view' +import { TransferAmountLoggingView } from './TransferAmountLogging.view' + +export class GradidoTransferLoggingView extends AbstractLoggingView { + public constructor(private self: GradidoTransfer) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + sender: new TransferAmountLoggingView(this.self.sender), + recipient: Buffer.from(this.self.recipient).toString(this.bufferStringFormat), + } + } +} diff --git a/dlt-connector/src/logging/GroupFriendsUpdateLogging.view.ts b/dlt-connector/src/logging/GroupFriendsUpdateLogging.view.ts new file mode 100644 index 000000000..8d1159d82 --- /dev/null +++ b/dlt-connector/src/logging/GroupFriendsUpdateLogging.view.ts @@ -0,0 +1,16 @@ +import { GroupFriendsUpdate } from '@/data/proto/3_3/GroupFriendsUpdate' + +import { AbstractLoggingView } from './AbstractLogging.view' + +export class GroupFriendsUpdateLoggingView extends AbstractLoggingView { + public constructor(private self: GroupFriendsUpdate) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + colorFusion: this.self.colorFusion, + } + } +} diff --git a/dlt-connector/src/logging/RegisterAddressLogging.view.ts b/dlt-connector/src/logging/RegisterAddressLogging.view.ts new file mode 100644 index 000000000..bb857e2b8 --- /dev/null +++ b/dlt-connector/src/logging/RegisterAddressLogging.view.ts @@ -0,0 +1,22 @@ +import { AddressType } from '@/data/proto/3_3/enum/AddressType' +import { RegisterAddress } from '@/data/proto/3_3/RegisterAddress' +import { getEnumValue } from '@/utils/typeConverter' + +import { AbstractLoggingView } from './AbstractLogging.view' + +export class RegisterAddressLoggingView extends AbstractLoggingView { + public constructor(private self: RegisterAddress) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + userPublicKey: Buffer.from(this.self.userPubkey).toString(this.bufferStringFormat), + addressType: getEnumValue(AddressType, this.self.addressType), + nameHash: Buffer.from(this.self.nameHash).toString(this.bufferStringFormat), + accountPublicKey: Buffer.from(this.self.accountPubkey).toString(this.bufferStringFormat), + derivationIndex: this.self.derivationIndex, + } + } +} diff --git a/dlt-connector/src/logging/SignatureMapLogging.view.ts b/dlt-connector/src/logging/SignatureMapLogging.view.ts new file mode 100644 index 000000000..93feb46f9 --- /dev/null +++ b/dlt-connector/src/logging/SignatureMapLogging.view.ts @@ -0,0 +1,17 @@ +import { SignatureMap } from '@/data/proto/3_3/SignatureMap' + +import { AbstractLoggingView } from './AbstractLogging.view' +import { SignaturePairLoggingView } from './SignaturePairLogging.view' + +export class SignatureMapLoggingView extends AbstractLoggingView { + public constructor(private self: SignatureMap) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + sigPair: this.self.sigPair.map((value) => new SignaturePairLoggingView(value).toJSON()), + } + } +} diff --git a/dlt-connector/src/logging/SignaturePairLogging.view.ts b/dlt-connector/src/logging/SignaturePairLogging.view.ts new file mode 100644 index 000000000..e88406098 --- /dev/null +++ b/dlt-connector/src/logging/SignaturePairLogging.view.ts @@ -0,0 +1,18 @@ +import { SignaturePair } from '@/data/proto/3_3/SignaturePair' + +import { AbstractLoggingView } from './AbstractLogging.view' + +export class SignaturePairLoggingView extends AbstractLoggingView { + public constructor(private self: SignaturePair) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + pubkey: Buffer.from(this.self.pubKey).toString(this.bufferStringFormat), + signature: + Buffer.from(this.self.signature).subarray(0, 31).toString(this.bufferStringFormat) + '..', + } + } +} diff --git a/dlt-connector/src/logging/TransactionBodyLogging.view.ts b/dlt-connector/src/logging/TransactionBodyLogging.view.ts new file mode 100644 index 000000000..0c287b0a5 --- /dev/null +++ b/dlt-connector/src/logging/TransactionBodyLogging.view.ts @@ -0,0 +1,46 @@ +import { CrossGroupType } from '@/data/proto/3_3/enum/CrossGroupType' +import { TransactionBody } from '@/data/proto/3_3/TransactionBody' +import { getEnumValue } from '@/utils/typeConverter' + +import { AbstractLoggingView } from './AbstractLogging.view' +import { CommunityRootLoggingView } from './CommunityRootLogging.view' +import { GradidoCreationLoggingView } from './GradidoCreationLogging.view' +import { GradidoDeferredTransferLoggingView } from './GradidoDeferredTransferLogging.view' +import { GradidoTransferLoggingView } from './GradidoTransferLogging.view' +import { GroupFriendsUpdateLoggingView } from './GroupFriendsUpdateLogging.view' +import { RegisterAddressLoggingView } from './RegisterAddressLogging.view' + +export class TransactionBodyLoggingView extends AbstractLoggingView { + public constructor(private self: TransactionBody) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + memo: this.self.memo, + createdAt: this.timestampToDateString(this.self.createdAt), + versionNumber: this.self.versionNumber, + type: getEnumValue(CrossGroupType, this.self.type), + otherGroup: this.self.otherGroup, + transfer: this.self.transfer + ? new GradidoTransferLoggingView(this.self.transfer).toJSON() + : undefined, + creation: this.self.creation + ? new GradidoCreationLoggingView(this.self.creation).toJSON() + : undefined, + groupFriendsUpdate: this.self.groupFriendsUpdate + ? new GroupFriendsUpdateLoggingView(this.self.groupFriendsUpdate).toJSON() + : undefined, + registerAddress: this.self.registerAddress + ? new RegisterAddressLoggingView(this.self.registerAddress).toJSON() + : undefined, + deferredTransfer: this.self.deferredTransfer + ? new GradidoDeferredTransferLoggingView(this.self.deferredTransfer).toJSON() + : undefined, + communityRoot: this.self.communityRoot + ? new CommunityRootLoggingView(this.self.communityRoot).toJSON() + : undefined, + } + } +} diff --git a/dlt-connector/src/logging/TransactionDraftLogging.view.ts b/dlt-connector/src/logging/TransactionDraftLogging.view.ts new file mode 100644 index 000000000..5e86822ec --- /dev/null +++ b/dlt-connector/src/logging/TransactionDraftLogging.view.ts @@ -0,0 +1,25 @@ +import { InputTransactionType } from '@/graphql/enum/InputTransactionType' +import { TransactionDraft } from '@/graphql/input/TransactionDraft' +import { getEnumValue } from '@/utils/typeConverter' + +import { AbstractLoggingView } from './AbstractLogging.view' +import { UserIdentifierLoggingView } from './UserIdentifierLogging.view' + +export class TransactionDraftLoggingView extends AbstractLoggingView { + public constructor(private self: TransactionDraft) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + user: new UserIdentifierLoggingView(this.self.user).toJSON(), + linkedUser: new UserIdentifierLoggingView(this.self.linkedUser).toJSON(), + backendTransactionId: this.self.backendTransactionId, + amount: this.decimalToString(this.self.amount), + type: getEnumValue(InputTransactionType, this.self.type), + createdAt: this.self.createdAt, + targetDate: this.self.targetDate, + } + } +} diff --git a/dlt-connector/src/logging/TransactionLogging.view.ts b/dlt-connector/src/logging/TransactionLogging.view.ts new file mode 100644 index 000000000..38443024d --- /dev/null +++ b/dlt-connector/src/logging/TransactionLogging.view.ts @@ -0,0 +1,59 @@ +import { Transaction } from '@entity/Transaction' + +import { TransactionType } from '@/data/proto/3_3/enum/TransactionType' +import { LogError } from '@/server/LogError' +import { getEnumValue } from '@/utils/typeConverter' + +import { AbstractLoggingView } from './AbstractLogging.view' +import { AccountLoggingView } from './AccountLogging.view' +import { BackendTransactionLoggingView } from './BackendTransactionLogging.view' +import { CommunityLoggingView } from './CommunityLogging.view' + +export class TransactionLoggingView extends AbstractLoggingView { + public constructor(private self: Transaction) { + super() + if (this.self.community === undefined) { + throw new LogError('sender community is zero') + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(showBackendTransactions = true): any { + return { + id: this.self.id, + nr: this.self.nr, + bodyBytesLength: this.self.bodyBytes.length, + createdAt: this.dateToString(this.self.createdAt), + confirmedAt: this.dateToString(this.self.confirmedAt), + protocolVersion: this.self.protocolVersion, + type: getEnumValue(TransactionType, this.self.type), + signature: this.self.signature.subarray(0, 31).toString(this.bufferStringFormat) + '..', + community: new CommunityLoggingView(this.self.community).toJSON(), + otherCommunity: this.self.otherCommunity + ? new CommunityLoggingView(this.self.otherCommunity) + : undefined, + iotaMessageId: this.self.iotaMessageId + ? this.self.iotaMessageId.toString(this.bufferStringFormat) + : undefined, + signingAccount: this.self.signingAccount + ? new AccountLoggingView(this.self.signingAccount) + : undefined, + recipientAccount: this.self.recipientAccount + ? new AccountLoggingView(this.self.recipientAccount) + : undefined, + amount: this.decimalToString(this.self.amount), + accountBalanceOnCreation: this.decimalToString(this.self.accountBalanceOnCreation), + accountBalanceOnConfirmation: this.decimalToString(this.self.accountBalanceOnConfirmation), + runningHash: this.self.runningHash + ? this.self.runningHash.toString(this.bufferStringFormat) + : undefined, + iotaMilestone: this.self.iotaMilestone, + backendTransactions: + showBackendTransactions && this.self.backendTransactions + ? this.self.backendTransactions.map((backendTransaction) => + new BackendTransactionLoggingView(backendTransaction).toJSON(false), + ) + : undefined, + } + } +} diff --git a/dlt-connector/src/logging/TransferAmountLogging.view.ts b/dlt-connector/src/logging/TransferAmountLogging.view.ts new file mode 100644 index 000000000..8d320b99f --- /dev/null +++ b/dlt-connector/src/logging/TransferAmountLogging.view.ts @@ -0,0 +1,18 @@ +import { TransferAmount } from '@/data/proto/3_3/TransferAmount' + +import { AbstractLoggingView } from './AbstractLogging.view' + +export class TransferAmountLoggingView extends AbstractLoggingView { + public constructor(private self: TransferAmount) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + publicKey: Buffer.from(this.self.pubkey).toString(this.bufferStringFormat), + amount: this.self.amount, + communityId: this.self.communityId, + } + } +} diff --git a/dlt-connector/src/logging/UserIdentifierLogging.view.ts b/dlt-connector/src/logging/UserIdentifierLogging.view.ts new file mode 100644 index 000000000..54ac4b07d --- /dev/null +++ b/dlt-connector/src/logging/UserIdentifierLogging.view.ts @@ -0,0 +1,18 @@ +import { UserIdentifier } from '@/graphql/input/UserIdentifier' + +import { AbstractLoggingView } from './AbstractLogging.view' + +export class UserIdentifierLoggingView extends AbstractLoggingView { + public constructor(private self: UserIdentifier) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + uuid: this.self.uuid, + communityUuid: this.self.communityUuid, + accountNr: this.self.accountNr, + } + } +} diff --git a/dlt-connector/src/logging/UserLogging.view.ts b/dlt-connector/src/logging/UserLogging.view.ts new file mode 100644 index 000000000..a3cbd66bc --- /dev/null +++ b/dlt-connector/src/logging/UserLogging.view.ts @@ -0,0 +1,20 @@ +import { User } from '@entity/User' + +import { AbstractLoggingView } from './AbstractLogging.view' + +export class UserLoggingView extends AbstractLoggingView { + public constructor(private user: User) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + id: this.user.id, + gradidoId: this.user.gradidoID, + derive1Pubkey: this.user.derive1Pubkey.toString(this.bufferStringFormat), + createdAt: this.dateToString(this.user.createdAt), + confirmedAt: this.dateToString(this.user.confirmedAt), + } + } +} diff --git a/dlt-connector/src/server/logger.ts b/dlt-connector/src/logging/logger.ts similarity index 99% rename from dlt-connector/src/server/logger.ts rename to dlt-connector/src/logging/logger.ts index 89757f656..bec2ec578 100644 --- a/dlt-connector/src/server/logger.ts +++ b/dlt-connector/src/logging/logger.ts @@ -1,7 +1,9 @@ +import { readFileSync } from 'fs' + import log4js from 'log4js' + import { CONFIG } from '@/config' -import { readFileSync } from 'fs' const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8')) log4js.configure(options) diff --git a/dlt-connector/src/proto/3_3/GradidoCreation.ts b/dlt-connector/src/proto/3_3/GradidoCreation.ts deleted file mode 100644 index ba6e93652..000000000 --- a/dlt-connector/src/proto/3_3/GradidoCreation.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Field, Message } from '@apollo/protobufjs' - -import { TimestampSeconds } from './TimestampSeconds' -import { TransferAmount } from './TransferAmount' -import { TransactionDraft } from '@/graphql/input/TransactionDraft' -import { TransactionError } from '@/graphql/model/TransactionError' -import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' - -// need signature from group admin or -// percent of group users another than the receiver -// https://www.npmjs.com/package/@apollo/protobufjs -// eslint-disable-next-line no-use-before-define -export class GradidoCreation extends Message { - constructor(transaction: TransactionDraft) { - if (!transaction.targetDate) { - throw new TransactionError( - TransactionErrorType.MISSING_PARAMETER, - 'missing targetDate for contribution', - ) - } - super({ - recipient: new TransferAmount({ amount: transaction.amount.toString() }), - targetDate: new TimestampSeconds(new Date(transaction.targetDate)), - }) - } - - @Field.d(1, TransferAmount) - public recipient: TransferAmount - - @Field.d(3, 'TimestampSeconds') - public targetDate: TimestampSeconds -} diff --git a/dlt-connector/src/proto/3_3/GradidoTransaction.ts b/dlt-connector/src/proto/3_3/GradidoTransaction.ts deleted file mode 100644 index ca1a59e30..000000000 --- a/dlt-connector/src/proto/3_3/GradidoTransaction.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Field, Message } from '@apollo/protobufjs' - -import { SignatureMap } from './SignatureMap' - -// https://www.npmjs.com/package/@apollo/protobufjs -// eslint-disable-next-line no-use-before-define -export class GradidoTransaction extends Message { - @Field.d(1, SignatureMap) - public sigMap: SignatureMap - - // inspired by Hedera - // bodyBytes are the payload for signature - // bodyBytes are serialized TransactionBody - @Field.d(2, 'bytes') - public bodyBytes: Buffer - - // if it is a cross group transaction the parent message - // id from outbound transaction or other by cross - @Field.d(3, 'bytes') - public parentMessageId: Buffer -} diff --git a/dlt-connector/src/proto/3_3/GradidoTransfer.ts b/dlt-connector/src/proto/3_3/GradidoTransfer.ts deleted file mode 100644 index 215ffc60f..000000000 --- a/dlt-connector/src/proto/3_3/GradidoTransfer.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Field, Message } from '@apollo/protobufjs' - -import { TransferAmount } from './TransferAmount' -import { TransactionDraft } from '@/graphql/input/TransactionDraft' - -// https://www.npmjs.com/package/@apollo/protobufjs -// eslint-disable-next-line no-use-before-define -export class GradidoTransfer extends Message { - constructor(transaction: TransactionDraft, coinOrigin?: string) { - super({ - sender: new TransferAmount({ - amount: transaction.amount.toString(), - communityId: coinOrigin, - }), - }) - } - - @Field.d(1, TransferAmount) - public sender: TransferAmount - - @Field.d(2, 'bytes') - public recipient: Buffer -} diff --git a/dlt-connector/src/proto/3_3/RegisterAddress.ts b/dlt-connector/src/proto/3_3/RegisterAddress.ts deleted file mode 100644 index 85b8390df..000000000 --- a/dlt-connector/src/proto/3_3/RegisterAddress.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Field, Message } from '@apollo/protobufjs' - -import { AddressType } from '@enum/AddressType' - -// https://www.npmjs.com/package/@apollo/protobufjs -// eslint-disable-next-line no-use-before-define -export class RegisterAddress extends Message { - @Field.d(1, 'bytes') - public userPubkey: Buffer - - @Field.d(2, 'AddressType') - public addressType: AddressType - - @Field.d(3, 'bytes') - public nameHash: Buffer - - @Field.d(4, 'bytes') - public subaccountPubkey: Buffer -} diff --git a/dlt-connector/src/proto/3_3/TransactionBody.ts b/dlt-connector/src/proto/3_3/TransactionBody.ts deleted file mode 100644 index 9e9179b3f..000000000 --- a/dlt-connector/src/proto/3_3/TransactionBody.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Field, Message, OneOf } from '@apollo/protobufjs' - -import { CrossGroupType } from '@/graphql/enum/CrossGroupType' - -import { Timestamp } from './Timestamp' -import { GradidoTransfer } from './GradidoTransfer' -import { GradidoCreation } from './GradidoCreation' -import { GradidoDeferredTransfer } from './GradidoDeferredTransfer' -import { GroupFriendsUpdate } from './GroupFriendsUpdate' -import { RegisterAddress } from './RegisterAddress' -import { TransactionDraft } from '@/graphql/input/TransactionDraft' -import { determineCrossGroupType, determineOtherGroup } from '@/controller/TransactionBody' - -// https://www.npmjs.com/package/@apollo/protobufjs -// eslint-disable-next-line no-use-before-define -export class TransactionBody extends Message { - public constructor(transaction: TransactionDraft) { - const type = determineCrossGroupType(transaction) - super({ - memo: 'Not implemented yet', - createdAt: new Timestamp(new Date(transaction.createdAt)), - versionNumber: '3.3', - type, - otherGroup: determineOtherGroup(type, transaction), - }) - } - - @Field.d(1, 'string') - public memo: string - - @Field.d(2, Timestamp) - public createdAt: Timestamp - - @Field.d(3, 'string') - public versionNumber: string - - @Field.d(4, CrossGroupType) - public type: CrossGroupType - - @Field.d(5, 'string') - public otherGroup: string - - @OneOf.d( - 'gradidoTransfer', - 'gradidoCreation', - 'groupFriendsUpdate', - 'registerAddress', - 'gradidoDeferredTransfer', - ) - public data: string - - @Field.d(6, 'GradidoTransfer') - transfer?: GradidoTransfer - - @Field.d(7, 'GradidoCreation') - creation?: GradidoCreation - - @Field.d(8, 'GroupFriendsUpdate') - groupFriendsUpdate?: GroupFriendsUpdate - - @Field.d(9, 'RegisterAddress') - registerAddress?: RegisterAddress - - @Field.d(10, 'GradidoDeferredTransfer') - deferredTransfer?: GradidoDeferredTransfer -} diff --git a/dlt-connector/src/server/LogError.ts b/dlt-connector/src/server/LogError.ts index 8e145a0ef..69aca1978 100644 --- a/dlt-connector/src/server/LogError.ts +++ b/dlt-connector/src/server/LogError.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -import { logger } from './logger' +import { logger } from '@/logging/logger' export class LogError extends Error { // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/dlt-connector/src/server/createServer.ts b/dlt-connector/src/server/createServer.ts index 00ba0a912..50e8d96cb 100755 --- a/dlt-connector/src/server/createServer.ts +++ b/dlt-connector/src/server/createServer.ts @@ -2,15 +2,17 @@ import 'reflect-metadata' import { ApolloServer } from '@apollo/server' import { expressMiddleware } from '@apollo/server/express4' -import express, { Express } from 'express' - -// graphql -import { schema } from '@/graphql/schema' - -import { logger as dltLogger } from './logger' -import { Logger } from 'log4js' -import cors from 'cors' import bodyParser from 'body-parser' +import cors from 'cors' +import express, { Express } from 'express' +// graphql +import { slowDown } from 'express-slow-down' +import helmet from 'helmet' +import { Logger } from 'log4js' + +import { schema } from '@/graphql/schema' +import { logger as dltLogger } from '@/logging/logger' +import { Connection } from '@/typeorm/DataSource' type ServerDef = { apollo: ApolloServer; app: Express } @@ -27,6 +29,8 @@ const createServer = async ( logger.addContext('user', 'unknown') logger.debug('createServer...') + // connect to db and test db version + await Connection.getInstance().init() // Express Server const app = express() @@ -38,6 +42,27 @@ const createServer = async ( // plugins logger, }) + // Helmet helps secure Express apps by setting HTTP response headers. + app.use(helmet()) + + // rate limiter/ slow down to many requests + const limiter = slowDown({ + windowMs: 1000, // 1 second + delayAfter: 10, // Allow 10 requests per 1 second. + delayMs: (hits) => hits * 50, // Add 100 ms of delay to every request after the 10th one. + /** + * So: + * + * - requests 1-10 are not delayed. + * - request 11 is delayed by 550ms + * - request 12 is delayed by 600ms + * - request 13 is delayed by 650ms + * + * and so on. After 1 seconds, the delay is reset to 0. + */ + }) + app.use(limiter) + await apollo.start() app.use( '/', diff --git a/dlt-connector/src/typeorm/DBVersion.ts b/dlt-connector/src/typeorm/DBVersion.ts deleted file mode 100644 index 14da39368..000000000 --- a/dlt-connector/src/typeorm/DBVersion.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Migration } from '@entity/Migration' - -import { logger } from '@/server/logger' - -const getDBVersion = async (): Promise => { - try { - const [dbVersion] = await Migration.find({ order: { version: 'DESC' }, take: 1 }) - return dbVersion ? dbVersion.fileName : null - } catch (error) { - logger.error(error) - return null - } -} - -const checkDBVersion = async (DB_VERSION: string): Promise => { - const dbVersion = await getDBVersion() - if (!dbVersion?.includes(DB_VERSION)) { - logger.error( - `Wrong database version detected - the backend requires '${DB_VERSION}' but found '${ - dbVersion ?? 'None' - }`, - ) - return false - } - return true -} - -export { checkDBVersion, getDBVersion } diff --git a/dlt-connector/src/typeorm/DataSource.ts b/dlt-connector/src/typeorm/DataSource.ts index eafa977aa..a86a061f3 100644 --- a/dlt-connector/src/typeorm/DataSource.ts +++ b/dlt-connector/src/typeorm/DataSource.ts @@ -2,25 +2,88 @@ // We cannot use our connection here, but must use the external typeorm installation import { DataSource as DBDataSource, FileLogger } from '@dbTools/typeorm' import { entities } from '@entity/index' +import { Migration } from '@entity/Migration' import { CONFIG } from '@/config' +import { logger } from '@/logging/logger' +import { LogError } from '@/server/LogError' -const DataSource = new DBDataSource({ - type: 'mysql', - host: CONFIG.DB_HOST, - port: CONFIG.DB_PORT, - username: CONFIG.DB_USER, - password: CONFIG.DB_PASSWORD, - database: CONFIG.DB_DATABASE, - entities, - synchronize: false, - logging: true, - logger: new FileLogger('all', { - logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH, - }), - extra: { - charset: 'utf8mb4_unicode_ci', - }, -}) +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class Connection { + // eslint-disable-next-line no-use-before-define + private static instance: Connection + private connection: DBDataSource -export const getDataSource = () => DataSource + /** + * 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() { + this.connection = new DBDataSource({ + type: 'mysql', + host: CONFIG.DB_HOST, + port: CONFIG.DB_PORT, + username: CONFIG.DB_USER, + password: CONFIG.DB_PASSWORD, + database: CONFIG.DB_DATABASE, + entities, + synchronize: false, + logging: true, + logger: new FileLogger('all', { + logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH, + }), + extra: { + charset: 'utf8mb4_unicode_ci', + }, + }) + } + + /** + * 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(): Connection { + if (!Connection.instance) { + Connection.instance = new Connection() + } + return Connection.instance + } + + public getDataSource(): DBDataSource { + return this.connection + } + + public async init(): Promise { + await this.connection.initialize() + try { + Connection.getInstance() + } catch (error) { + // try and catch for logging + logger.fatal(`Couldn't open connection to database!`) + throw error + } + + // check for correct database version + await this.checkDBVersion(CONFIG.DB_VERSION) + } + + async checkDBVersion(DB_VERSION: string): Promise { + const dbVersion = await Migration.find({ order: { version: 'DESC' }, take: 1 }) + if (!dbVersion || dbVersion.length < 1) { + throw new LogError('found no db version in migrations, could dlt-database run successfully?') + } + // return dbVersion ? dbVersion.fileName : null + if (!dbVersion[0].fileName.includes(DB_VERSION)) { + throw new LogError( + `Wrong database version detected - the backend requires '${DB_VERSION}' but found '${ + dbVersion[0].fileName ?? 'None' + }`, + ) + } + } +} + +export const getDataSource = () => Connection.getInstance().getDataSource() diff --git a/dlt-connector/src/utils/derivationHelper.test.ts b/dlt-connector/src/utils/derivationHelper.test.ts new file mode 100644 index 000000000..f14b99cdf --- /dev/null +++ b/dlt-connector/src/utils/derivationHelper.test.ts @@ -0,0 +1,16 @@ +import 'reflect-metadata' +import { Timestamp } from '../data/proto/3_3/Timestamp' + +import { hardenDerivationIndex, HARDENED_KEY_BITMASK } from './derivationHelper' +import { timestampToDate } from './typeConverter' + +describe('utils', () => { + it('test bitmask for hardened keys', () => { + const derivationIndex = hardenDerivationIndex(1) + expect(derivationIndex).toBeGreaterThan(HARDENED_KEY_BITMASK) + }) + it('test TimestampToDate', () => { + const date = new Date('2011-04-17T12:01:10.109') + expect(timestampToDate(new Timestamp(date))).toEqual(date) + }) +}) diff --git a/dlt-connector/src/utils/derivationHelper.ts b/dlt-connector/src/utils/derivationHelper.ts new file mode 100644 index 000000000..0431ec339 --- /dev/null +++ b/dlt-connector/src/utils/derivationHelper.ts @@ -0,0 +1,17 @@ +export const HARDENED_KEY_BITMASK = 0x80000000 + +/* + * change derivation index from x => x' + * for more infos to hardened keys look here: + * https://en.bitcoin.it/wiki/BIP_0032 + */ +export const hardenDerivationIndex = (derivationIndex: number): number => { + /* + TypeScript uses signed integers by default, + but bip32-ed25519 expects an unsigned value for the derivation index. + The >>> shifts the bits 0 places to the right, which effectively makes no change to the value, + but forces TypeScript to treat derivationIndex as an unsigned value. + Source: ChatGPT + */ + return (derivationIndex | HARDENED_KEY_BITMASK) >>> 0 +} diff --git a/dlt-connector/src/utils/typeConverter.test.ts b/dlt-connector/src/utils/typeConverter.test.ts new file mode 100644 index 000000000..d9b1c2356 --- /dev/null +++ b/dlt-connector/src/utils/typeConverter.test.ts @@ -0,0 +1,13 @@ +import 'reflect-metadata' +import { Timestamp } from '@/data/proto/3_3/Timestamp' + +import { timestampToDate } from './typeConverter' + +describe('utils/typeConverter', () => { + it('timestampToDate', () => { + const now = new Date('Thu, 05 Oct 2023 11:55:18 +0000') + const timestamp = new Timestamp(now) + expect(timestamp.seconds).toBe(Math.round(now.getTime() / 1000)) + expect(timestampToDate(timestamp)).toEqual(now) + }) +}) diff --git a/dlt-connector/src/utils/typeConverter.ts b/dlt-connector/src/utils/typeConverter.ts index bd9e5f8da..52dcd2a98 100644 --- a/dlt-connector/src/utils/typeConverter.ts +++ b/dlt-connector/src/utils/typeConverter.ts @@ -1,5 +1,15 @@ import { crypto_generichash as cryptoHash } from 'sodium-native' +import { AddressType } from '@/data/proto/3_3/enum/AddressType' +import { Timestamp } from '@/data/proto/3_3/Timestamp' +import { TimestampSeconds } from '@/data/proto/3_3/TimestampSeconds' +import { TransactionBody } from '@/data/proto/3_3/TransactionBody' +import { AccountType } from '@/graphql/enum/AccountType' +import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' +import { TransactionError } from '@/graphql/model/TransactionError' +import { logger } from '@/logging/logger' +import { LogError } from '@/server/LogError' + export const uuid4ToBuffer = (uuid: string): Buffer => { // Remove dashes from the UUIDv4 string const cleanedUUID = uuid.replace(/-/g, '') @@ -15,3 +25,83 @@ export const iotaTopicFromCommunityUUID = (communityUUID: string): string => { cryptoHash(hash, uuid4ToBuffer(communityUUID)) return hash.toString('hex') } + +export const timestampToDate = (timestamp: Timestamp): Date => { + let milliseconds = timestamp.nanoSeconds / 1000000 + milliseconds += timestamp.seconds * 1000 + return new Date(milliseconds) +} + +export const timestampSecondsToDate = (timestamp: TimestampSeconds): Date => { + return new Date(timestamp.seconds * 1000) +} + +export const base64ToBuffer = (base64: string): Buffer => { + return Buffer.from(base64, 'base64') +} + +export const bodyBytesToTransactionBody = (bodyBytes: Buffer): TransactionBody => { + 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', + ) + } +} + +export const transactionBodyToBodyBytes = (transactionBody: TransactionBody): Buffer => { + try { + return Buffer.from(TransactionBody.encode(transactionBody).finish()) + } catch (error) { + logger.error('error encoding transaction body to body bytes', error) + throw new TransactionError( + TransactionErrorType.PROTO_ENCODE_ERROR, + 'cannot encode transaction body', + ) + } +} + +export function getEnumValue>( + enumType: T, + value: number | string, +): T[keyof T] | undefined { + if (typeof value === 'number' && typeof enumType === 'object') { + return enumType[value as keyof T] as T[keyof T] + } else if (typeof value === 'string') { + for (const key in enumType) { + if (enumType[key as keyof T] === value) { + return enumType[key as keyof T] as T[keyof T] + } + } + } + return undefined +} + +export const accountTypeToAddressType = (type: AccountType): AddressType => { + const typeString: string = AccountType[type] + const addressType: AddressType = AddressType[typeString as keyof typeof AddressType] + + if (!addressType) { + throw new LogError("couldn't find corresponding AddressType for AccountType", { + accountType: type, + addressTypes: Object.keys(AddressType), + }) + } + return addressType +} + +export const addressTypeToAccountType = (type: AddressType): AccountType => { + const typeString: string = AddressType[type] + const accountType: AccountType = AccountType[typeString as keyof typeof AccountType] + + if (!accountType) { + throw new LogError("couldn't find corresponding AccountType for AddressType", { + addressTypes: type, + accountType: Object.keys(AccountType), + }) + } + return accountType +} diff --git a/dlt-connector/test/ApolloServerMock.ts b/dlt-connector/test/ApolloServerMock.ts index 6e0585862..c13df2407 100644 --- a/dlt-connector/test/ApolloServerMock.ts +++ b/dlt-connector/test/ApolloServerMock.ts @@ -1,5 +1,6 @@ import { ApolloServer } from '@apollo/server' import { addMocksToSchema } from '@graphql-tools/mock' + import { schema } from '@/graphql/schema' let apolloTestServer: ApolloServer diff --git a/dlt-connector/test/TestDB.ts b/dlt-connector/test/TestDB.ts index 457954f7d..63ce78500 100644 --- a/dlt-connector/test/TestDB.ts +++ b/dlt-connector/test/TestDB.ts @@ -1,11 +1,11 @@ import { DataSource, FileLogger } from '@dbTools/typeorm' -import { createDatabase } from 'typeorm-extension' - import { entities } from '@entity/index' +import { createDatabase } from 'typeorm-extension' import { CONFIG } from '@/config' import { LogError } from '@/server/LogError' +// TODO: maybe use in memory db like here: https://dkzeb.medium.com/unit-testing-in-ts-jest-with-typeorm-entities-ad5de5f95438 export class TestDB { // eslint-disable-next-line no-use-before-define private static _instance: TestDB diff --git a/dlt-connector/test/testSetup.ts b/dlt-connector/test/testSetup.ts index ff619e95d..71170cbf0 100644 --- a/dlt-connector/test/testSetup.ts +++ b/dlt-connector/test/testSetup.ts @@ -1,9 +1,9 @@ -import { logger } from '@/server/logger' +import { logger } from '@/logging/logger' jest.setTimeout(1000000) -jest.mock('@/server/logger', () => { - const originalModule = jest.requireActual('@/server/logger') +jest.mock('@/logging/logger', () => { + const originalModule = jest.requireActual('@/logging/logger') return { __esModule: true, ...originalModule, diff --git a/dlt-connector/tsconfig.json b/dlt-connector/tsconfig.json index 3abf9aead..e37b2a7a0 100644 --- a/dlt-connector/tsconfig.json +++ b/dlt-connector/tsconfig.json @@ -55,8 +55,7 @@ "@resolver/*": ["src/graphql/resolver/*"], "@scalar/*": ["src/graphql/scalar/*"], "@test/*": ["test/*"], - "@proto/*" : ["src/proto/*"], - "@controller/*": ["src/controller/*"], + "@proto/*" : ["src/proto/*"], "@validator/*" : ["src/graphql/validator/*"], "@typeorm/*" : ["src/typeorm/*"], /* external */ diff --git a/dlt-connector/yarn.lock b/dlt-connector/yarn.lock index 136e845f5..6d50426b1 100644 --- a/dlt-connector/yarn.lock +++ b/dlt-connector/yarn.lock @@ -20,7 +20,7 @@ resolved "https://registry.yarnpkg.com/@apollo/cache-control-types/-/cache-control-types-1.0.3.tgz#5da62cf64c3b4419dabfef4536b57a40c8ff0b47" integrity sha512-F17/vCp7QVwom9eG7ToauIKdAxpSoadsJnqIfyryLFSkLSOEqu+eC5Z3N8OXcUVStuOMcNHlyraRsA6rRICu4g== -"@apollo/protobufjs@1.2.7", "@apollo/protobufjs@^1.2.7": +"@apollo/protobufjs@1.2.7": version "1.2.7" resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.2.7.tgz#3a8675512817e4a046a897e5f4f16415f16a7d8a" integrity sha512-Lahx5zntHPZia35myYDBRuF58tlwPskwHc5CWBZC/4bMKB6siTBWwtMrkqXcsNwQiFSzSx5hKdRPUmemrEp3Gg== @@ -843,6 +843,11 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@noble/hashes@^1.2.0": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1125,6 +1130,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.7.0.tgz#c03de4572f114a940bc2ca909a33ddb2b925e470" integrity sha512-zI22/pJW2wUZOVyguFaUL1HABdmSVxpXrzIqkjsHmyUjNhPoWM1CKfvVuXfetHhIok4RY573cqS0mZ1SJEnoTg== +"@types/node@>=13.7.0": + version "20.8.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.7.tgz#ad23827850843de973096edfc5abc9e922492a25" + integrity sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ== + dependencies: + undici-types "~5.25.1" + "@types/node@^18.11.18": version "18.18.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.0.tgz#bd19d5133a6e5e2d0152ec079ac27c120e7f1763" @@ -1628,6 +1640,22 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +bip32-ed25519@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/bip32-ed25519/-/bip32-ed25519-0.0.4.tgz#218943e212c2d3152dfd6f3a929305e3fe86534c" + integrity sha512-KfazzGVLwl70WZ1r98dO+8yaJRTGgWHL9ITn4bXHQi2mB4cT3Hjh53tXWUpEWE1zKCln7PbyX8Z337VapAOb5w== + dependencies: + bn.js "^5.1.1" + elliptic "^6.4.1" + hash.js "^1.1.7" + +bip39@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.1.0.tgz#c55a418deaf48826a6ceb34ac55b3ee1577e18a3" + integrity sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A== + dependencies: + "@noble/hashes" "^1.2.0" + bl@^4.0.3: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -1637,6 +1665,16 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" +bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + +bn.js@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + body-parser@1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -1711,6 +1749,11 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== + browser-process-hrtime@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" @@ -2343,6 +2386,19 @@ electron-to-chromium@^1.4.530: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.531.tgz#22966d894c4680726c17cf2908ee82ff5d26ac25" integrity sha512-H6gi5E41Rn3/mhKlPaT1aIMg/71hTAqn0gYEllSuw9igNWtvQwu185jiCZoZD29n7Zukgh7GVZ3zGf0XvkhqjQ== +elliptic@^6.4.1: + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + emittery@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" @@ -2530,6 +2586,11 @@ eslint-module-utils@^2.7.4, eslint-module-utils@^2.8.0: dependencies: debug "^3.2.7" +eslint-plugin-dci-lint@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-dci-lint/-/eslint-plugin-dci-lint-0.3.0.tgz#dcd73c50505b589b415017cdb72716f98e9495c3" + integrity sha512-BhgrwJ5k3eMN41NwCZ/tYQGDTMOrHXpH8XOfRZrGtPqmlnOZCVGWow+KyZMz0/wOFVpXx/q9B0y7R7qtU7lnqg== + eslint-plugin-es@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz#f0822f0c18a535a97c3e714e89f88586a7641ec9" @@ -2772,6 +2833,18 @@ expect@^27.5.1: jest-matcher-utils "^27.5.1" jest-message-util "^27.5.1" +express-rate-limit@7: + version "7.1.5" + resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-7.1.5.tgz#af4c81143a945ea97f2599d13957440a0ddbfcfe" + integrity sha512-/iVogxu7ueadrepw1bS0X0kaRC/U0afwiYRSLg68Ts+p4Dc85Q5QKsOnPS/QUjPMHvOJQtBDrZgvkOzf8ejUYw== + +express-slow-down@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/express-slow-down/-/express-slow-down-2.0.1.tgz#60c4515467314675d89c54ec608e2d586aa30f87" + integrity sha512-zRogSZhNXJYKDBekhgFfFXGrOngH7Fub7Mx2g8OQ4RUBwSJP/3TVEKMgSGR/WlneT0mJ6NBUnidHhIELGVPe3w== + dependencies: + express-rate-limit "7" + express@4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" @@ -3338,11 +3411,33 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +helmet@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-7.1.0.tgz#287279e00f8a3763d5dccbaf1e5ee39b8c3784ca" + integrity sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg== + highlight.js@^10.7.1: version "10.7.3" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531" integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== +hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + html-encoding-sniffer@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" @@ -4379,6 +4474,11 @@ long@^4.0.0: resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== +long@^5.0.0: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== + lower-case@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" @@ -4494,6 +4594,16 @@ mimic-response@^2.0.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== + minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -5038,6 +5148,24 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +protobufjs@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.5.tgz#45d5c57387a6d29a17aab6846dcc283f9b8e7f2d" + integrity sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + proxy-addr@~2.0.5, proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -6202,6 +6330,11 @@ undefsafe@^2.0.5: resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== +undici-types@~5.25.1: + version "5.25.3" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.25.3.tgz#e044115914c85f0bcbb229f346ab739f064998c3" + integrity sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA== + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" diff --git a/dlt-database/entity/0001-init_db/Account.ts b/dlt-database/entity/0001-init_db/Account.ts index 43910122a..7ceaf09cc 100644 --- a/dlt-database/entity/0001-init_db/Account.ts +++ b/dlt-database/entity/0001-init_db/Account.ts @@ -8,8 +8,10 @@ import { BaseEntity, } from 'typeorm' import { User } from '../User' -import { TransactionRecipe } from '../TransactionRecipe' -import { ConfirmedTransaction } from '../ConfirmedTransaction' +// TransactionRecipe was removed in newer migrations, so only the version from this folder can be linked +import { TransactionRecipe } from './TransactionRecipe' +// ConfirmedTransaction was removed in newer migrations, so only the version from this folder can be linked +import { ConfirmedTransaction } from './ConfirmedTransaction' import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' import { Decimal } from 'decimal.js-light' import { AccountCommunity } from '../AccountCommunity' diff --git a/dlt-database/entity/0001-init_db/Community.ts b/dlt-database/entity/0001-init_db/Community.ts index 96ddd59d6..943914878 100644 --- a/dlt-database/entity/0001-init_db/Community.ts +++ b/dlt-database/entity/0001-init_db/Community.ts @@ -8,7 +8,8 @@ import { BaseEntity, } from 'typeorm' import { Account } from '../Account' -import { TransactionRecipe } from '../TransactionRecipe' +// TransactionRecipe was removed in newer migrations, so only the version from this folder can be linked +import { TransactionRecipe } from './TransactionRecipe' import { AccountCommunity } from '../AccountCommunity' @Entity('communities') diff --git a/dlt-database/entity/0001-init_db/ConfirmedTransaction.ts b/dlt-database/entity/0001-init_db/ConfirmedTransaction.ts index 16786a713..408a58a69 100644 --- a/dlt-database/entity/0001-init_db/ConfirmedTransaction.ts +++ b/dlt-database/entity/0001-init_db/ConfirmedTransaction.ts @@ -10,8 +10,10 @@ import { import { Decimal } from 'decimal.js-light' import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' -import { Account } from '../Account' -import { TransactionRecipe } from '../TransactionRecipe' +// the relation in future account don't match which this any longer, so we can only link with the local account here +import { Account } from './Account' +// TransactionRecipe was removed in newer migrations, so only the version from this folder can be linked +import { TransactionRecipe } from './TransactionRecipe' @Entity('confirmed_transactions') export class ConfirmedTransaction extends BaseEntity { diff --git a/dlt-database/entity/0001-init_db/TransactionRecipe.ts b/dlt-database/entity/0001-init_db/TransactionRecipe.ts index 934e81d02..b2acbba75 100644 --- a/dlt-database/entity/0001-init_db/TransactionRecipe.ts +++ b/dlt-database/entity/0001-init_db/TransactionRecipe.ts @@ -10,9 +10,12 @@ import { import { Decimal } from 'decimal.js-light' import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' -import { Account } from '../Account' -import { Community } from '../Community' -import { ConfirmedTransaction } from '../ConfirmedTransaction' +// the relation in future account don't match which this any longer, so we can only link with the local account here +import { Account } from './Account' +// the relation in future community don't match which this any longer, so we can only link with the local account here +import { Community } from './Community' +// ConfirmedTransaction was removed in newer migrations, so only the version from this folder can be linked +import { ConfirmedTransaction } from './ConfirmedTransaction' @Entity('transaction_recipes') export class TransactionRecipe extends BaseEntity { diff --git a/dlt-database/entity/0002-refactor_add_community/Account.ts b/dlt-database/entity/0002-refactor_add_community/Account.ts index 9edba933d..821b75e73 100644 --- a/dlt-database/entity/0002-refactor_add_community/Account.ts +++ b/dlt-database/entity/0002-refactor_add_community/Account.ts @@ -8,8 +8,10 @@ import { BaseEntity, } from 'typeorm' import { User } from '../User' -import { TransactionRecipe } from '../TransactionRecipe' -import { ConfirmedTransaction } from '../ConfirmedTransaction' +// TransactionRecipe was removed in newer migrations, so only the version from this folder can be linked +import { TransactionRecipe } from '../0001-init_db/TransactionRecipe' +// ConfirmedTransaction was removed in newer migrations, so only the version from this folder can be linked +import { ConfirmedTransaction } from './ConfirmedTransaction' import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' import { Decimal } from 'decimal.js-light' import { AccountCommunity } from '../AccountCommunity' diff --git a/dlt-database/entity/0002-refactor_add_community/Community.ts b/dlt-database/entity/0002-refactor_add_community/Community.ts index 25f9e3265..7136efa2e 100644 --- a/dlt-database/entity/0002-refactor_add_community/Community.ts +++ b/dlt-database/entity/0002-refactor_add_community/Community.ts @@ -8,7 +8,8 @@ import { BaseEntity, } from 'typeorm' import { Account } from '../Account' -import { TransactionRecipe } from '../TransactionRecipe' +// TransactionRecipe was removed in newer migrations, so only the version from this folder can be linked +import { TransactionRecipe } from '../0001-init_db/TransactionRecipe' import { AccountCommunity } from '../AccountCommunity' @Entity('communities') diff --git a/dlt-database/entity/0002-refactor_add_community/ConfirmedTransaction.ts b/dlt-database/entity/0002-refactor_add_community/ConfirmedTransaction.ts index 5d2a38f65..1cdc591bf 100644 --- a/dlt-database/entity/0002-refactor_add_community/ConfirmedTransaction.ts +++ b/dlt-database/entity/0002-refactor_add_community/ConfirmedTransaction.ts @@ -10,8 +10,10 @@ import { import { Decimal } from 'decimal.js-light' import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' -import { Account } from '../Account' -import { TransactionRecipe } from '../TransactionRecipe' +// the relation in future account don't match which this any longer, so we can only link with the local account here +import { Account } from './Account' +// TransactionRecipe was removed in newer migrations, so only the version from this folder can be linked +import { TransactionRecipe } from '../0001-init_db/TransactionRecipe' @Entity('confirmed_transactions') export class ConfirmedTransaction extends BaseEntity { diff --git a/dlt-database/entity/0003-refactor_transaction_recipe/Account.ts b/dlt-database/entity/0003-refactor_transaction_recipe/Account.ts new file mode 100644 index 000000000..1c01094f1 --- /dev/null +++ b/dlt-database/entity/0003-refactor_transaction_recipe/Account.ts @@ -0,0 +1,89 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, + OneToMany, + BaseEntity, +} from 'typeorm' +import { User } from '../User' +import { Transaction } from '../Transaction' +import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' +import { Decimal } from 'decimal.js-light' +import { AccountCommunity } from '../AccountCommunity' + +@Entity('accounts') +export class Account extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @ManyToOne(() => User, (user) => user.accounts, { cascade: ['insert', 'update'], eager: true }) // Assuming you have a User entity with 'accounts' relation + @JoinColumn({ name: 'user_id' }) + user?: User + + // if user id is null, account belongs to community gmw or auf + @Column({ name: 'user_id', type: 'int', unsigned: true, nullable: true }) + userId?: number + + @Column({ name: 'derivation_index', type: 'int', unsigned: true }) + derivationIndex: number + + @Column({ name: 'derive2_pubkey', type: 'binary', length: 32, unique: true }) + derive2Pubkey: Buffer + + @Column({ type: 'tinyint', unsigned: true }) + type: number + + @Column({ name: 'created_at', type: 'datetime', precision: 3 }) + createdAt: Date + + // 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 + + @Column({ + name: 'balance_on_confirmation', + type: 'decimal', + precision: 40, + scale: 20, + default: 0, + transformer: DecimalTransformer, + }) + balanceOnConfirmation: Decimal + + // use timestamp from iota milestone which is only in seconds precision, so no need to use 3 Bytes extra here + @Column({ + name: 'balance_confirmed_at', + type: 'datetime', + nullable: true, + }) + balanceConfirmedAt: Date + + @Column({ + name: 'balance_on_creation', + type: 'decimal', + precision: 40, + scale: 20, + default: 0, + transformer: DecimalTransformer, + }) + balanceOnCreation: Decimal + + @Column({ + name: 'balance_created_at', + type: 'datetime', + precision: 3, + }) + balanceCreatedAt: Date + + @OneToMany(() => AccountCommunity, (accountCommunity) => accountCommunity.account) + @JoinColumn({ name: 'account_id' }) + accountCommunities: AccountCommunity[] + + @OneToMany(() => Transaction, (transaction) => transaction.signingAccount) + transactionSigning?: Transaction[] + + @OneToMany(() => Transaction, (transaction) => transaction.recipientAccount) + transactionRecipient?: Transaction[] +} diff --git a/dlt-database/entity/0003-refactor_transaction_recipe/BackendTransaction.ts b/dlt-database/entity/0003-refactor_transaction_recipe/BackendTransaction.ts new file mode 100644 index 000000000..c84a15f41 --- /dev/null +++ b/dlt-database/entity/0003-refactor_transaction_recipe/BackendTransaction.ts @@ -0,0 +1,45 @@ +import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToOne, JoinColumn } from 'typeorm' +import { Decimal } from 'decimal.js-light' + +import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' +import { Transaction } from '../Transaction' + +@Entity('backend_transactions') +export class BackendTransaction extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true, type: 'bigint' }) + id: number + + @Column({ name: 'backend_transaction_id', type: 'bigint', unsigned: true, unique: true }) + backendTransactionId: number + + @ManyToOne(() => Transaction, (transaction) => transaction.backendTransactions) + @JoinColumn({ name: 'transaction_id' }) + transaction: Transaction + + @Column({ name: 'transaction_id', type: 'bigint', unsigned: true }) + transactionId: number + + @Column({ name: 'type_id', unsigned: true, nullable: false }) + typeId: number + + // account balance based on creation date + @Column({ + name: 'balance', + type: 'decimal', + precision: 40, + scale: 20, + nullable: true, + transformer: DecimalTransformer, + }) + balance?: Decimal + + @Column({ name: 'created_at', type: 'datetime', precision: 3 }) + createdAt: Date + + // 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 + + @Column({ name: 'verifiedOnBackend', type: 'tinyint', default: false }) + verifiedOnBackend: boolean +} diff --git a/dlt-database/entity/0003-refactor_transaction_recipe/Community.ts b/dlt-database/entity/0003-refactor_transaction_recipe/Community.ts new file mode 100644 index 000000000..1233d0832 --- /dev/null +++ b/dlt-database/entity/0003-refactor_transaction_recipe/Community.ts @@ -0,0 +1,64 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + JoinColumn, + OneToOne, + OneToMany, + BaseEntity, +} from 'typeorm' +import { Account } from '../Account' +import { Transaction } from '../Transaction' +import { AccountCommunity } from '../AccountCommunity' + +@Entity('communities') +export class Community extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ name: 'iota_topic', collation: 'utf8mb4_unicode_ci', unique: true }) + iotaTopic: string + + @Column({ name: 'root_pubkey', type: 'binary', length: 32, unique: true, nullable: true }) + rootPubkey?: Buffer + + @Column({ name: 'root_privkey', type: 'binary', length: 64, nullable: true }) + rootPrivkey?: Buffer + + @Column({ name: 'root_chaincode', type: 'binary', length: 32, nullable: true }) + rootChaincode?: Buffer + + @Column({ type: 'tinyint', default: true }) + foreign: boolean + + @Column({ name: 'gmw_account_id', type: 'int', unsigned: true, nullable: true }) + gmwAccountId?: number + + @OneToOne(() => Account, { cascade: true }) + @JoinColumn({ name: 'gmw_account_id' }) + gmwAccount?: Account + + @Column({ name: 'auf_account_id', type: 'int', unsigned: true, nullable: true }) + aufAccountId?: number + + @OneToOne(() => Account, { cascade: true }) + @JoinColumn({ name: 'auf_account_id' }) + aufAccount?: Account + + @Column({ name: 'created_at', type: 'datetime', precision: 3 }) + createdAt: Date + + // 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(() => AccountCommunity, (accountCommunity) => accountCommunity.community) + @JoinColumn({ name: 'community_id' }) + accountCommunities: AccountCommunity[] + + @OneToMany(() => Transaction, (transaction) => transaction.community) + transactions?: Transaction[] + + @OneToMany(() => Transaction, (transaction) => transaction.otherCommunity) + friendCommunitiesTransactions?: Transaction[] +} diff --git a/dlt-database/entity/0003-refactor_transaction_recipe/InvalidTransaction.ts b/dlt-database/entity/0003-refactor_transaction_recipe/InvalidTransaction.ts new file mode 100644 index 000000000..a34823dbd --- /dev/null +++ b/dlt-database/entity/0003-refactor_transaction_recipe/InvalidTransaction.ts @@ -0,0 +1,13 @@ +import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm' + +@Entity('invalid_transactions') +export class InvalidTransaction extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true, type: 'bigint' }) + id: number + + @Column({ name: 'iota_message_id', type: 'binary', length: 32, unique: true }) + iotaMessageId: Buffer + + @Column({ name: 'error_message', type: 'varchar', length: 255 }) + errorMessage: string +} diff --git a/dlt-database/entity/0003-refactor_transaction_recipe/Transaction.ts b/dlt-database/entity/0003-refactor_transaction_recipe/Transaction.ts new file mode 100644 index 000000000..922bf81cd --- /dev/null +++ b/dlt-database/entity/0003-refactor_transaction_recipe/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) + // eslint-disable-next-line no-use-before-define + paringTransaction?: Transaction + + @Column({ name: 'paring_transaction_id', type: 'bigint', unsigned: true, nullable: true }) + paringTransactionId?: 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/0003-refactor_transaction_recipe/User.ts b/dlt-database/entity/0003-refactor_transaction_recipe/User.ts new file mode 100644 index 000000000..fdfeb9830 --- /dev/null +++ b/dlt-database/entity/0003-refactor_transaction_recipe/User.ts @@ -0,0 +1,35 @@ +import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToMany, JoinColumn } from 'typeorm' + +import { Account } from '../Account' + +@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) +export class User extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ + name: 'gradido_id', + length: 36, + nullable: true, + collation: 'utf8mb4_unicode_ci', + }) + gradidoID?: string + + @Column({ name: 'derive1_pubkey', type: 'binary', length: 32, unique: true }) + derive1Pubkey: Buffer + + @Column({ name: 'created_at', type: 'datetime', precision: 3 }) + createdAt: Date + + // 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(() => Account, (account) => account.user) + @JoinColumn({ name: 'user_id' }) + accounts?: Account[] +} diff --git a/dlt-database/entity/Account.ts b/dlt-database/entity/Account.ts index ed1e92840..3d7713ba9 100644 --- a/dlt-database/entity/Account.ts +++ b/dlt-database/entity/Account.ts @@ -1 +1 @@ -export { Account } from './0002-refactor_add_community/Account' +export { Account } from './0003-refactor_transaction_recipe/Account' diff --git a/dlt-database/entity/BackendTransaction.ts b/dlt-database/entity/BackendTransaction.ts new file mode 100644 index 000000000..6ec68427d --- /dev/null +++ b/dlt-database/entity/BackendTransaction.ts @@ -0,0 +1 @@ +export { BackendTransaction } from './0003-refactor_transaction_recipe/BackendTransaction' diff --git a/dlt-database/entity/Community.ts b/dlt-database/entity/Community.ts index 211837e40..cb4d34c43 100644 --- a/dlt-database/entity/Community.ts +++ b/dlt-database/entity/Community.ts @@ -1 +1 @@ -export { Community } from './0002-refactor_add_community/Community' +export { Community } from './0003-refactor_transaction_recipe/Community' diff --git a/dlt-database/entity/ConfirmedTransaction.ts b/dlt-database/entity/ConfirmedTransaction.ts deleted file mode 100644 index 765e0b2e6..000000000 --- a/dlt-database/entity/ConfirmedTransaction.ts +++ /dev/null @@ -1 +0,0 @@ -export { ConfirmedTransaction } from './0002-refactor_add_community/ConfirmedTransaction' diff --git a/dlt-database/entity/InvalidTransaction.ts b/dlt-database/entity/InvalidTransaction.ts index 8042e74b4..166b13adf 100644 --- a/dlt-database/entity/InvalidTransaction.ts +++ b/dlt-database/entity/InvalidTransaction.ts @@ -1 +1 @@ -export { InvalidTransaction } from './0001-init_db/InvalidTransaction' +export { InvalidTransaction } from './0003-refactor_transaction_recipe/InvalidTransaction' diff --git a/dlt-database/entity/Transaction.ts b/dlt-database/entity/Transaction.ts new file mode 100644 index 000000000..113eb3450 --- /dev/null +++ b/dlt-database/entity/Transaction.ts @@ -0,0 +1 @@ +export { Transaction } from './0003-refactor_transaction_recipe/Transaction' diff --git a/dlt-database/entity/TransactionRecipe.ts b/dlt-database/entity/TransactionRecipe.ts deleted file mode 100644 index e59a09ef9..000000000 --- a/dlt-database/entity/TransactionRecipe.ts +++ /dev/null @@ -1 +0,0 @@ -export { TransactionRecipe } from './0001-init_db/TransactionRecipe' diff --git a/dlt-database/entity/User.ts b/dlt-database/entity/User.ts index 3c803d783..4f1b039ff 100644 --- a/dlt-database/entity/User.ts +++ b/dlt-database/entity/User.ts @@ -1 +1 @@ -export { User } from './0002-refactor_add_community/User' +export { User } from './0003-refactor_transaction_recipe/User' diff --git a/dlt-database/entity/index.ts b/dlt-database/entity/index.ts index 74c2e2258..b1215263d 100644 --- a/dlt-database/entity/index.ts +++ b/dlt-database/entity/index.ts @@ -1,19 +1,19 @@ import { Account } from './Account' import { AccountCommunity } from './AccountCommunity' +import { BackendTransaction } from './BackendTransaction' import { Community } from './Community' -import { ConfirmedTransaction } from './ConfirmedTransaction' import { InvalidTransaction } from './InvalidTransaction' import { Migration } from './Migration' -import { TransactionRecipe } from './TransactionRecipe' +import { Transaction } from './Transaction' import { User } from './User' export const entities = [ AccountCommunity, Account, + BackendTransaction, Community, - ConfirmedTransaction, InvalidTransaction, Migration, - TransactionRecipe, + Transaction, User, ] diff --git a/dlt-database/migrations/0001-init_db.ts b/dlt-database/migrations/0001-init_db.ts index 85fed59e0..8188a889d 100644 --- a/dlt-database/migrations/0001-init_db.ts +++ b/dlt-database/migrations/0001-init_db.ts @@ -23,7 +23,7 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis \`confirmed_at\` datetime(3) DEFAULT NULL, PRIMARY KEY (\`id\`), INDEX \`gradido_id\` (\`gradido_id\`), - UNIQUE KEY \`pubkey\` (\`pubkey\`) + UNIQUE KEY \`derive1_pubkey\` (\`derive1_pubkey\`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`) await queryFn(` @@ -38,7 +38,7 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis \`balance\` decimal(40,20) NOT NULL DEFAULT 0, \`balance_date\` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (\`id\`), - UNIQUE KEY \`pubkey\` (\`pubkey\`), + UNIQUE KEY \`derive2_pubkey\` (\`derive2_pubkey\`), FOREIGN KEY (\`user_id\`) REFERENCES users(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; `) @@ -56,7 +56,7 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis \`created_at\` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), \`confirmed_at\` datetime(3) DEFAULT NULL, PRIMARY KEY (\`id\`), - UNIQUE KEY \`pubkey\` (\`pubkey\`), + UNIQUE KEY \`root_pubkey\` (\`root_pubkey\`), FOREIGN KEY (\`gmw_account_id\`) REFERENCES accounts(id), FOREIGN KEY (\`auf_account_id\`) REFERENCES accounts(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`) diff --git a/dlt-database/migrations/0003-refactor_transaction_recipe.ts b/dlt-database/migrations/0003-refactor_transaction_recipe.ts new file mode 100644 index 000000000..0c022cc42 --- /dev/null +++ b/dlt-database/migrations/0003-refactor_transaction_recipe.ts @@ -0,0 +1,156 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + // write upgrade logic as parameter of queryFn + await queryFn(`DROP TABLE \`confirmed_transactions\`;`) + await queryFn(`DROP TABLE \`transaction_recipes\`;`) + + await queryFn(` + ALTER TABLE \`accounts\` + RENAME COLUMN \`balance\` TO \`balance_on_confirmation\`, + RENAME COLUMN \`balance_date\` TO \`balance_confirmed_at\` + ; + `) + + await queryFn( + `ALTER TABLE \`accounts\` ADD COLUMN \`balance_on_creation\` decimal(40,20) NOT NULL DEFAULT 0 AFTER \`balance_confirmed_at\`;`, + ) + await queryFn( + `ALTER TABLE \`accounts\` ADD COLUMN \`balance_created_at\` datetime(3) NOT NULL AFTER \`balance_on_creation\`;`, + ) + await queryFn( + `ALTER TABLE \`accounts\` MODIFY COLUMN \`balance_confirmed_at\` datetime NULL DEFAULT NULL;`, + ) + + await queryFn( + `ALTER TABLE \`invalid_transactions\` ADD COLUMN \`error_message\` varchar(255) NOT NULL;`, + ) + + await queryFn(`ALTER TABLE \`invalid_transactions\` DROP INDEX \`iota_message_id\`;`) + await queryFn(`ALTER TABLE \`invalid_transactions\` ADD UNIQUE(\`iota_message_id\`);`) + + await queryFn( + `CREATE TABLE \`transactions\` ( + \`id\` bigint unsigned NOT NULL AUTO_INCREMENT, + \`iota_message_id\` varbinary(32) NULL DEFAULT NULL, + \`paring_transaction_id\` bigint unsigned NULL DEFAULT NULL, + \`signing_account_id\` int unsigned NULL DEFAULT NULL, + \`recipient_account_id\` int unsigned NULL DEFAULT NULL, + \`community_id\` int unsigned NOT NULL, + \`other_community_id\` int unsigned NULL DEFAULT NULL, + \`amount\` decimal(40, 20) NULL DEFAULT NULL, + \`account_balance_on_creation\` decimal(40, 20) NULL DEFAULT 0.00000000000000000000, + \`type\` tinyint NOT NULL, + \`created_at\` datetime(3) NOT NULL, + \`body_bytes\` blob NOT NULL, + \`signature\` varbinary(64) NOT NULL, + \`protocol_version\` varchar(255) NOT NULL DEFAULT '1', + \`nr\` bigint NULL DEFAULT NULL, + \`running_hash\` varbinary(48) NULL DEFAULT NULL, + \`account_balance_on_confirmation\` decimal(40, 20) NULL DEFAULT 0.00000000000000000000, + \`iota_milestone\` bigint NULL DEFAULT NULL, + \`confirmed_at\` datetime NULL DEFAULT NULL, + PRIMARY KEY (\`id\`), + UNIQUE KEY \`signature\` (\`signature\`), + FOREIGN KEY (\`signing_account_id\`) REFERENCES accounts(id), + FOREIGN KEY (\`recipient_account_id\`) REFERENCES accounts(id), + FOREIGN KEY (\`community_id\`) REFERENCES communities(id), + FOREIGN KEY (\`other_community_id\`) REFERENCES communities(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `, + ) + + await queryFn( + `CREATE TABLE \`backend_transactions\` ( + \`id\` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, + \`backend_transaction_id\` BIGINT UNSIGNED NOT NULL, + \`transaction_id\` BIGINT UNSIGNED NOT NULL, + \`type_id\` INT UNSIGNED NOT NULL, + \`balance\` DECIMAL(40, 20) NULL DEFAULT NULL, + \`created_at\` DATETIME(3) NOT NULL, + \`confirmed_at\` DATETIME NULL DEFAULT NULL, + \`verifiedOnBackend\` TINYINT NOT NULL DEFAULT 0, + PRIMARY KEY (\`id\`), + UNIQUE (\`backend_transaction_id\`), + FOREIGN KEY (\`transaction_id\`) REFERENCES transactions(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + `, + ) + + await queryFn(`ALTER TABLE \`communities\` ADD UNIQUE(\`iota_topic\`);`) + + await queryFn(`ALTER TABLE \`users\` CHANGE \`created_at\` \`created_at\` DATETIME(3) NOT NULL;`) + await queryFn( + `ALTER TABLE \`communities\` CHANGE \`created_at\` \`created_at\` DATETIME(3) NOT NULL;`, + ) + await queryFn( + `ALTER TABLE \`accounts\` CHANGE \`created_at\` \`created_at\` DATETIME(3) NOT NULL;`, + ) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn(` + CREATE TABLE IF NOT EXISTS \`transaction_recipes\` ( + \`id\` bigint unsigned NOT NULL AUTO_INCREMENT, + \`iota_message_id\` binary(32) DEFAULT NULL, + \`signing_account_id\` int(10) unsigned NOT NULL, + \`recipient_account_id\` int(10) unsigned DEFAULT NULL, + \`sender_community_id\` int(10) unsigned NOT NULL, + \`recipient_community_id\` int(10) unsigned DEFAULT NULL, + \`amount\` decimal(40,20) DEFAULT NULL, + \`type\` tinyint unsigned NOT NULL, + \`created_at\` datetime(3) NOT NULL, + \`body_bytes\` BLOB NOT NULL, + \`signature\` binary(64) NOT NULL, + \`protocol_version\` int(10) NOT NULL DEFAULT 1, + PRIMARY KEY (\`id\`), + FOREIGN KEY (\`signing_account_id\`) REFERENCES accounts(id), + FOREIGN KEY (\`recipient_account_id\`) REFERENCES accounts(id), + FOREIGN KEY (\`sender_community_id\`) REFERENCES communities(id), + FOREIGN KEY (\`recipient_community_id\`) REFERENCES communities(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`) + + await queryFn(` + CREATE TABLE IF NOT EXISTS \`confirmed_transactions\` ( + \`id\` bigint unsigned NOT NULL AUTO_INCREMENT, + \`transaction_recipe_id\` bigint unsigned NOT NULL, + \`nr\` bigint unsigned NOT NULL, + \`running_hash\` binary(48) NOT NULL, + \`account_id\` int(10) unsigned NOT NULL, + \`account_balance\` decimal(40,20) NOT NULL DEFAULT 0, + \`iota_milestone\` bigint NOT NULL, + \`confirmed_at\` datetime NOT NULL, + PRIMARY KEY (\`id\`), + FOREIGN KEY (\`transaction_recipe_id\`) REFERENCES transaction_recipes(id), + FOREIGN KEY (\`account_id\`) REFERENCES accounts(id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`) + + await queryFn( + `ALTER TABLE \`accounts\` MODIFY COLUMN \`balance_confirmed_at_date\` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3);`, + ) + await queryFn(` + ALTER TABLE \`accounts\` + RENAME COLUMN \`balance_on_confirmation\` TO \`balance\`, + RENAME COLUMN \`balance_confirmed_at\` TO \`balance_date\` + ; + `) + + await queryFn(`ALTER TABLE \`accounts\` DROP COLUMN \`balance_on_creation\`;`) + await queryFn(`ALTER TABLE \`accounts\` DROP COLUMN \`balance_created_at\`;`) + await queryFn(`ALTER TABLE \`invalid_transactions\` DROP COLUMN \`error_message\`;`) + await queryFn(`ALTER TABLE \`invalid_transactions\` DROP INDEX \`iota_message_id\`;`) + await queryFn(`ALTER TABLE \`invalid_transactions\` ADD INDEX(\`iota_message_id\`); `) + await queryFn(`DROP TABLE \`transactions\`;`) + await queryFn(`DROP TABLE \`backend_transactions\`;`) + + await queryFn( + `ALTER TABLE \`users\` CHANGE \`created_at\` \`created_at\` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3);`, + ) + await queryFn( + `ALTER TABLE \`communities\` CHANGE \`created_at\` \`created_at\` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3);`, + ) + await queryFn( + `ALTER TABLE \`accounts\` CHANGE \`created_at\` \`created_at\` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3);`, + ) +} diff --git a/dlt-database/src/config/index.ts b/dlt-database/src/config/index.ts index 20208befc..46a1e580c 100644 --- a/dlt-database/src/config/index.ts +++ b/dlt-database/src/config/index.ts @@ -13,19 +13,19 @@ const constants = { } const database = { - DB_HOST: process.env.DB_HOST || 'localhost', + DB_HOST: process.env.DB_HOST ?? 'localhost', DB_PORT: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306, - DB_USER: process.env.DB_USER || 'root', - DB_PASSWORD: process.env.DB_PASSWORD || '', - DB_DATABASE: process.env.DB_DATABASE || 'gradido_dlt', + DB_USER: process.env.DB_USER ?? 'root', + DB_PASSWORD: process.env.DB_PASSWORD ?? '', + DB_DATABASE: process.env.DB_DATABASE ?? 'gradido_dlt', } const migrations = { - MIGRATIONS_TABLE: process.env.MIGRATIONS_TABLE || 'migrations', + MIGRATIONS_TABLE: process.env.MIGRATIONS_TABLE ?? 'migrations', } // Check config version -constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT +constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION ?? constants.CONFIG_VERSION.DEFAULT if ( ![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes( constants.CONFIG_VERSION.CURRENT, diff --git a/federation/.env.template b/federation/.env.template index e6ac8ad7d..9a029c3d1 100644 --- a/federation/.env.template +++ b/federation/.env.template @@ -1,5 +1,5 @@ # must match the CONFIG_VERSION.EXPECTED definition in scr/config/index.ts -CONFIG_VERSION=v2.2023-08-24 +CONFIG_VERSION=$FEDERATION_CONFIG_VERSION LOG_LEVEL=$LOG_LEVEL # this is set fix to false, because it is important for 'production' environments. only set to true if a graphql-playground should be in use @@ -13,7 +13,8 @@ DB_PASSWORD=$DB_PASSWORD DB_DATABASE=gradido_community # Federation -FEDERATION_COMMUNITY_URL=$FEDERATION_COMMUNITY_URL +COMMUNITY_HOST=$COMMUNITY_HOST +URL_PROTOCOL=$URL_PROTOCOL FEDERATION_CONFIG_VERSION=$FEDERATION_CONFIG_VERSION # comma separated list of api-versions, which cause starting several federation modules FEDERATION_COMMUNITY_APIS=$FEDERATION_COMMUNITY_APIS \ No newline at end of file diff --git a/federation/jest.config.js b/federation/jest.config.js index bd41344f5..42bac0002 100644 --- a/federation/jest.config.js +++ b/federation/jest.config.js @@ -24,6 +24,11 @@ module.exports = { process.env.NODE_ENV === 'development' ? '/../database/entity/$1' : '/../database/build/entity/$1', + '@logging/(.*)': + // eslint-disable-next-line n/no-process-env + process.env.NODE_ENV === 'development' + ? '/../database/logging/$1' + : '/../database/build/logging/$1', '@dbTools/(.*)': process.env.NODE_ENV === 'development' ? '/../database/src/$1' diff --git a/federation/package.json b/federation/package.json index fa21e04a1..1457b1be1 100644 --- a/federation/package.json +++ b/federation/package.json @@ -24,8 +24,10 @@ "decimal.js-light": "^2.5.1", "dotenv": "10.0.0", "express": "4.17.1", + "express-slow-down": "^2.0.1", "graphql": "15.5.1", "graphql-request": "5.0.0", + "helmet": "^7.1.0", "lodash.clonedeep": "^4.5.0", "log4js": "^6.7.1", "reflect-metadata": "^0.1.13", diff --git a/federation/src/client/1_0/AuthenticationClient.ts b/federation/src/client/1_0/AuthenticationClient.ts index bed6b88c4..3a94746b1 100644 --- a/federation/src/client/1_0/AuthenticationClient.ts +++ b/federation/src/client/1_0/AuthenticationClient.ts @@ -63,7 +63,10 @@ export class AuthenticationClient { return authUuid } } catch (err) { - logger.error('Authentication: authenticate failed for endpoint', this.endpoint) + logger.error('Authentication: authenticate failed', { + endpoint: this.endpoint, + err, + }) } return null } diff --git a/federation/src/config/index.ts b/federation/src/config/index.ts index 16b246aef..ec6fdff26 100644 --- a/federation/src/config/index.ts +++ b/federation/src/config/index.ts @@ -10,11 +10,11 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0079-introduce_gms_registration', + DB_VERSION: '0082-introduce_gms_registration', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info - LOG_LEVEL: process.env.LOG_LEVEL || 'info', + LOG_LEVEL: process.env.LOG_LEVEL ?? 'info', CONFIG_VERSION: { DEFAULT: 'DEFAULT', EXPECTED: 'v2.2023-08-24', @@ -25,21 +25,21 @@ const constants = { const server = { // JWT_SECRET: process.env.JWT_SECRET || 'secret123', // JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m', - GRAPHIQL: process.env.GRAPHIQL === 'true' || false, + GRAPHIQL: process.env.GRAPHIQL === 'true' ?? false, // GDT_API_URL: process.env.GDT_API_URL || 'https://gdt.gradido.net', - PRODUCTION: process.env.NODE_ENV === 'production' || false, + PRODUCTION: process.env.NODE_ENV === 'production' ?? false, } const database = { - DB_HOST: process.env.DB_HOST || 'localhost', + DB_HOST: process.env.DB_HOST ?? 'localhost', DB_PORT: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306, - DB_USER: process.env.DB_USER || 'root', - DB_PASSWORD: process.env.DB_PASSWORD || '', - DB_DATABASE: process.env.DB_DATABASE || 'gradido_community', - TYPEORM_LOGGING_RELATIVE_PATH: process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.backend.log', + DB_USER: process.env.DB_USER ?? 'root', + DB_PASSWORD: process.env.DB_PASSWORD ?? '', + DB_DATABASE: process.env.DB_DATABASE ?? 'gradido_community', + TYPEORM_LOGGING_RELATIVE_PATH: process.env.TYPEORM_LOGGING_RELATIVE_PATH ?? 'typeorm.backend.log', } // Check config version -constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT +constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION ?? constants.CONFIG_VERSION.DEFAULT if ( ![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes( constants.CONFIG_VERSION.CURRENT, @@ -50,10 +50,14 @@ if ( ) } +const COMMUNITY_HOST = process.env.COMMUNITY_HOST ?? 'localhost' +const URL_PROTOCOL = process.env.URL_PROTOCOL ?? 'http' +const COMMUNITY_URL = process.env.COMMUNITY_URL ?? `${URL_PROTOCOL}://${COMMUNITY_HOST}` + const federation = { - FEDERATION_API: process.env.FEDERATION_API || '1_0', - FEDERATION_PORT: process.env.FEDERATION_PORT || 5010, - FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL || null, + FEDERATION_API: process.env.FEDERATION_API ?? '1_0', + FEDERATION_PORT: process.env.FEDERATION_PORT ?? 5010, + FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL ?? COMMUNITY_URL, FEDERATION_TRADING_LEVEL: { RECEIVER_COMMUNITY_URL: 'https://stage3.gradido.net/api/', SEND_COINS: true, diff --git a/federation/src/graphql/api/1_0/logger/GetPublicCommunityInfoResultLogging.view.ts b/federation/src/graphql/api/1_0/logger/GetPublicCommunityInfoResultLogging.view.ts new file mode 100644 index 000000000..669170b98 --- /dev/null +++ b/federation/src/graphql/api/1_0/logger/GetPublicCommunityInfoResultLogging.view.ts @@ -0,0 +1,18 @@ +import { GetPublicCommunityInfoResult } from '@/graphql/api/1_0/model/GetPublicCommunityInfoResult' +import { AbstractLoggingView } from '@logging/AbstractLogging.view' + +export class GetPublicCommunityInfoResultLoggingView extends AbstractLoggingView { + public constructor(private self: GetPublicCommunityInfoResult) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + name: this.self.name, + description: this.self.description, + creationDate: this.dateToString(this.self.creationDate), + publicKey: this.self.publicKey, + } + } +} diff --git a/federation/src/graphql/api/1_0/logger/SendCoinsArgsLogging.view.ts b/federation/src/graphql/api/1_0/logger/SendCoinsArgsLogging.view.ts new file mode 100644 index 000000000..a12ff6372 --- /dev/null +++ b/federation/src/graphql/api/1_0/logger/SendCoinsArgsLogging.view.ts @@ -0,0 +1,23 @@ +import { AbstractLoggingView } from '@logging/AbstractLogging.view' +import { SendCoinsArgs } from '@/graphql/api/1_0/model/SendCoinsArgs' + +export class SendCoinsArgsLoggingView extends AbstractLoggingView { + public constructor(private self: SendCoinsArgs) { + super() + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toJSON(): any { + return { + recipientCommunityUuid: this.self.recipientCommunityUuid, + recipientUserIdentifier: this.self.recipientUserIdentifier, + creationDate: this.self.creationDate, + amount: this.decimalToString(this.self.amount), + memoLength: this.self.memo.length, + senderCommunityUuid: this.self.senderCommunityUuid, + senderUserUuid: this.self.senderUserUuid, + senderUserName: this.self.senderUserName.substring(0, 3), + senderAlias: this.self.senderAlias?.substring(0, 3), + } + } +} diff --git a/federation/src/graphql/api/1_0/model/GetPublicCommunityInfoResult.ts b/federation/src/graphql/api/1_0/model/GetPublicCommunityInfoResult.ts index 86ea480df..d51b3af93 100644 --- a/federation/src/graphql/api/1_0/model/GetPublicCommunityInfoResult.ts +++ b/federation/src/graphql/api/1_0/model/GetPublicCommunityInfoResult.ts @@ -7,7 +7,7 @@ import { Field, ObjectType } from 'type-graphql' // eslint-disable-next-line @typescript-eslint/no-unused-vars export class GetPublicCommunityInfoResult { constructor(dbCom: DbCommunity) { - this.publicKey = dbCom.publicKey.toString() + this.publicKey = dbCom.publicKey.toString('hex') this.name = dbCom.name this.description = dbCom.description this.creationDate = dbCom.creationDate diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index 8f7b510cf..393fe3b65 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -3,6 +3,8 @@ import { Arg, Mutation, Resolver } from 'type-graphql' import { federationLogger as logger } from '@/server/logger' import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFedCommunity } from '@entity/FederatedCommunity' +import { CommunityLoggingView } from '@logging/CommunityLogging.view' +import { FederatedCommunityLoggingView } from '@logging/FederatedCommunityLogging.view' import { LogError } from '@/server/LogError' import { OpenConnectionArgs } from '../model/OpenConnectionArgs' import { startAuthentication, startOpenConnectionCallback } from '../util/authenticateCommunity' @@ -11,7 +13,6 @@ import { CONFIG } from '@/config' import { AuthenticationArgs } from '../model/AuthenticationArgs' @Resolver() -// eslint-disable-next-line @typescript-eslint/no-unused-vars export class AuthenticationResolver { @Mutation(() => Boolean) async openConnection( @@ -28,7 +29,7 @@ export class AuthenticationResolver { if (!comA) { throw new LogError(`unknown requesting community with publicKey`, pubKeyBuf.toString('hex')) } - logger.debug(`Authentication: found requestedCom:`, comA) + logger.debug(`Authentication: found requestedCom:`, new CommunityLoggingView(comA)) // no await to respond immediatly and invoke callback-request asynchron void startOpenConnectionCallback(args, comA, CONFIG.FEDERATION_API) return true @@ -48,7 +49,10 @@ export class AuthenticationResolver { if (!fedComB) { throw new LogError(`unknown callback community with url`, args.url) } - logger.debug(`Authentication: found fedComB and start authentication:`, fedComB) + logger.debug( + `Authentication: found fedComB and start authentication:`, + new FederatedCommunityLoggingView(fedComB), + ) // no await to respond immediatly and invoke authenticate-request asynchron void startAuthentication(args.oneTimeCode, fedComB) return true @@ -61,13 +65,16 @@ export class AuthenticationResolver { ): Promise { logger.debug(`Authentication: authenticate() via apiVersion=1_0 ...`, args) const authCom = await DbCommunity.findOneByOrFail({ communityUuid: args.oneTimeCode }) - logger.debug('Authentication: found authCom:', authCom) + logger.debug('Authentication: found authCom:', new CommunityLoggingView(authCom)) if (authCom) { // TODO decrypt args.uuid with authCom.publicKey authCom.communityUuid = args.uuid authCom.authenticatedAt = new Date() await DbCommunity.save(authCom) - logger.debug('Authentication: store authCom.uuid successfully:', authCom) + logger.debug( + 'Authentication: store authCom.uuid successfully:', + new CommunityLoggingView(authCom), + ) const homeCom = await DbCommunity.findOneByOrFail({ foreign: false }) // TODO encrypt homeCom.uuid with homeCom.privateKey if (homeCom.communityUuid) { diff --git a/federation/src/graphql/api/1_0/resolver/PublicCommunityInfoResolver.test.ts b/federation/src/graphql/api/1_0/resolver/PublicCommunityInfoResolver.test.ts index 08544834f..2f83b4819 100644 --- a/federation/src/graphql/api/1_0/resolver/PublicCommunityInfoResolver.test.ts +++ b/federation/src/graphql/api/1_0/resolver/PublicCommunityInfoResolver.test.ts @@ -46,7 +46,10 @@ describe('PublicCommunityInfoResolver', () => { homeCom.name = 'Community-Name' homeCom.description = 'Community-Description' homeCom.creationDate = new Date() - homeCom.publicKey = Buffer.from('homeCommunity-publicKey') + homeCom.publicKey = Buffer.from( + '316f2951501f27c664e188d5128505917e8673e8bebce141f86e70907e782a08', + 'hex', + ) await DbCommunity.insert(homeCom) }) @@ -57,7 +60,7 @@ describe('PublicCommunityInfoResolver', () => { name: 'Community-Name', description: 'Community-Description', creationDate: homeCom.creationDate?.toISOString(), - publicKey: expect.stringMatching('homeCommunity-publicKey'), + publicKey: '316f2951501f27c664e188d5128505917e8673e8bebce141f86e70907e782a08', }, }, }) diff --git a/federation/src/graphql/api/1_0/resolver/PublicCommunityInfoResolver.ts b/federation/src/graphql/api/1_0/resolver/PublicCommunityInfoResolver.ts index 339314f86..c1535b713 100644 --- a/federation/src/graphql/api/1_0/resolver/PublicCommunityInfoResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/PublicCommunityInfoResolver.ts @@ -3,16 +3,19 @@ import { Query, Resolver } from 'type-graphql' import { federationLogger as logger } from '@/server/logger' import { Community as DbCommunity } from '@entity/Community' import { GetPublicCommunityInfoResult } from '../model/GetPublicCommunityInfoResult' +import { GetPublicCommunityInfoResultLoggingView } from '../logger/GetPublicCommunityInfoResultLogging.view' @Resolver() -// eslint-disable-next-line @typescript-eslint/no-unused-vars export class PublicCommunityInfoResolver { @Query(() => GetPublicCommunityInfoResult) async getPublicCommunityInfo(): Promise { logger.debug(`getPublicCommunityInfo() via apiVersion=1_0 ...`) const homeCom = await DbCommunity.findOneByOrFail({ foreign: false }) const result = new GetPublicCommunityInfoResult(homeCom) - logger.debug(`getPublicCommunityInfo()-1_0... return publicInfo=${JSON.stringify(result)}`) + const publicInfoView = new GetPublicCommunityInfoResultLoggingView(result) + logger.debug( + `getPublicCommunityInfo()-1_0... return publicInfo=${publicInfoView.toString(true)}`, + ) return result } } diff --git a/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.test.ts b/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.test.ts index 83c024c9f..eafd9cba7 100644 --- a/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.test.ts +++ b/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.test.ts @@ -39,7 +39,10 @@ describe('PublicKeyResolver', () => { homeCom.foreign = false homeCom.apiVersion = '1_0' homeCom.endPoint = 'endpoint-url' - homeCom.publicKey = Buffer.from('homeCommunity-publicKey') + homeCom.publicKey = Buffer.from( + '9f6dcd0d985cc7105cd71c3417d9c291b126c8ca90513197de02191f928ef713', + 'hex', + ) await DbFederatedCommunity.insert(homeCom) }) @@ -47,7 +50,7 @@ describe('PublicKeyResolver', () => { await expect(query({ query: getPublicKeyQuery })).resolves.toMatchObject({ data: { getPublicKey: { - publicKey: expect.stringMatching('homeCommunity-publicKey'), + publicKey: '9f6dcd0d985cc7105cd71c3417d9c291b126c8ca90513197de02191f928ef713', }, }, }) diff --git a/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.ts b/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.ts index bab0e25f5..3fb3b2c0d 100644 --- a/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.ts @@ -5,7 +5,6 @@ import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCom import { GetPublicKeyResult } from '../model/GetPublicKeyResult' @Resolver() -// eslint-disable-next-line @typescript-eslint/no-unused-vars export class PublicKeyResolver { @Query(() => GetPublicKeyResult) async getPublicKey(): Promise { @@ -16,7 +15,8 @@ export class PublicKeyResolver { apiVersion: '1_0', }, }) - logger.debug(`getPublicKey()-1_0... return publicKey=${homeCom.publicKey}`) - return new GetPublicKeyResult(homeCom.publicKey.toString()) + const publicKeyHex = homeCom.publicKey.toString('hex') + logger.debug(`getPublicKey()-1_0... return publicKey=${publicKeyHex}`) + return new GetPublicKeyResult(publicKeyHex) } } diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts index aa6e2300e..e90a7818c 100644 --- a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts @@ -1,6 +1,6 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import { Arg, Args, Mutation, Resolver } from 'type-graphql' +import { Arg, Mutation, Resolver } from 'type-graphql' import { federationLogger as logger } from '@/server/logger' +import { PendingTransactionLoggingView } from '@logging/PendingTransactionLogging.view' import { Community as DbCommunity } from '@entity/Community' import { PendingTransaction as DbPendingTransaction } from '@entity/PendingTransaction' import { SendCoinsArgs } from '../model/SendCoinsArgs' @@ -16,27 +16,16 @@ import { findUserByIdentifier } from '@/graphql/util/findUserByIdentifier' import { SendCoinsResult } from '../model/SendCoinsResult' import Decimal from 'decimal.js-light' import { storeForeignUser } from '../util/storeForeignUser' +import { SendCoinsArgsLoggingView } from '../logger/SendCoinsArgsLogging.view' @Resolver() -// eslint-disable-next-line @typescript-eslint/no-unused-vars export class SendCoinsResolver { @Mutation(() => SendCoinsResult) async voteForSendCoins( @Arg('data') args: SendCoinsArgs, ): Promise { - logger.debug( - `voteForSendCoins() via apiVersion=1_0 ...`, - args.recipientCommunityUuid, - args.recipientUserIdentifier, - args.creationDate, - args.amount.toString(), - args.memo, - args.senderCommunityUuid, - args.senderUserUuid, - args.senderUserName, - args.senderAlias, - ) + logger.debug(`voteForSendCoins() via apiVersion=1_0 ...`, new SendCoinsArgsLoggingView(args)) const result = new SendCoinsResult() // first check if receiver community is correct const homeCom = await DbCommunity.findOneBy({ @@ -152,7 +141,10 @@ export class SendCoinsResolver { linkedUserCommunityUuid: args.senderCommunityUuid, linkedUserGradidoID: args.senderUserUuid, }) - logger.debug('XCom: revertSendCoins found pendingTX=', pendingTx) + logger.debug( + 'XCom: revertSendCoins found pendingTX=', + pendingTx ? new PendingTransactionLoggingView(pendingTx) : 'null', + ) if (pendingTx && pendingTx.amount.toString() === args.amount.toString()) { logger.debug('XCom: revertSendCoins matching pendingTX for remove...') try { @@ -167,19 +159,11 @@ export class SendCoinsResolver { pendingTx?.amount.toString(), args.amount.toString(), ) - throw new LogError( - `Can't find in revertSendCoins the pending receiver TX for args=`, - args.recipientCommunityUuid, - args.recipientUserIdentifier, - PendingTransactionState.NEW, - TransactionTypeId.RECEIVE, - args.creationDate, - args.amount, - args.memo, - args.senderCommunityUuid, - args.senderUserUuid, - args.senderUserName, - ) + throw new LogError(`Can't find in revertSendCoins the pending receiver TX for `, { + args: new SendCoinsArgsLoggingView(args), + pendingTransactionState: PendingTransactionState.NEW, + transactionType: TransactionTypeId.RECEIVE, + }) } logger.debug(`revertSendCoins()-1_0... successfull`) return true @@ -193,15 +177,7 @@ export class SendCoinsResolver { @Arg('data') args: SendCoinsArgs, ): Promise { - logger.debug( - `settleSendCoins() via apiVersion=1_0 ...userCommunityUuid=${ - args.recipientCommunityUuid - }, userGradidoID=${args.recipientUserIdentifier}, balanceDate=${ - args.creationDate - },amount=${args.amount.valueOf()}, memo=${args.memo}, linkedUserCommunityUuid = ${ - args.senderCommunityUuid - }, userSenderIdentifier=${args.senderUserUuid}, userSenderName=${args.senderUserName}`, - ) + logger.debug(`settleSendCoins() via apiVersion=1_0 ...`, new SendCoinsArgsLoggingView(args)) // first check if receiver community is correct const homeCom = await DbCommunity.findOneBy({ communityUuid: args.recipientCommunityUuid, @@ -232,7 +208,10 @@ export class SendCoinsResolver { linkedUserCommunityUuid: args.senderCommunityUuid, linkedUserGradidoID: args.senderUserUuid, }) - logger.debug('XCom: settleSendCoins found pendingTX=', pendingTx?.toString()) + logger.debug( + 'XCom: settleSendCoins found pendingTX=', + pendingTx ? new PendingTransactionLoggingView(pendingTx) : 'null', + ) if ( pendingTx && pendingTx.amount.toString() === args.amount.toString() && @@ -256,17 +235,12 @@ export class SendCoinsResolver { } else { logger.debug('XCom: settlePendingReceiveTransaction NOT matching pendingTX for settlement...') throw new LogError( - `Can't find in settlePendingReceiveTransaction the pending receiver TX for args=`, - args.recipientCommunityUuid, - args.recipientUserIdentifier, - PendingTransactionState.NEW, - TransactionTypeId.RECEIVE, - args.creationDate, - args.amount, - args.memo, - args.senderCommunityUuid, - args.senderUserUuid, - args.senderUserName, + `Can't find in settlePendingReceiveTransaction the pending receiver TX for `, + { + args: new SendCoinsArgsLoggingView(args), + pendingTransactionState: PendingTransactionState.NEW, + transactionTypeId: TransactionTypeId.RECEIVE, + }, ) } } @@ -307,7 +281,10 @@ export class SendCoinsResolver { linkedUserCommunityUuid: args.senderCommunityUuid, linkedUserGradidoID: args.senderUserUuid, }) - logger.debug('XCom: revertSettledSendCoins found pendingTX=', pendingTx) + logger.debug( + 'XCom: revertSettledSendCoins found pendingTX=', + pendingTx ? new PendingTransactionLoggingView(pendingTx) : 'null', + ) if ( pendingTx && pendingTx.amount.toString() === args.amount.toString() && @@ -322,19 +299,11 @@ export class SendCoinsResolver { } } else { logger.debug('XCom: revertSettledSendCoins NOT matching pendingTX...') - throw new LogError( - `Can't find in revertSettledSendCoins the pending receiver TX for args=`, - args.recipientCommunityUuid, - args.recipientUserIdentifier, - PendingTransactionState.SETTLED, - TransactionTypeId.RECEIVE, - args.creationDate, - args.amount, - args.memo, - args.senderCommunityUuid, - args.senderUserUuid, - args.senderUserName, - ) + throw new LogError(`Can't find in revertSettledSendCoins the pending receiver TX for `, { + args: new SendCoinsArgsLoggingView(args), + pendingTransactionState: PendingTransactionState.SETTLED, + transactionTypeId: TransactionTypeId.RECEIVE, + }) } logger.debug(`revertSendCoins()-1_0... successfull`) return true diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index 0af3475ef..1d3365d9c 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -9,13 +9,18 @@ import { AuthenticationClientFactory } from '@/client/AuthenticationClientFactor // eslint-disable-next-line camelcase import { AuthenticationClient as V1_0_AuthenticationClient } from '@/client/1_0/AuthenticationClient' import { AuthenticationArgs } from '../model/AuthenticationArgs' +import { CommunityLoggingView } from '@logging/CommunityLogging.view' +import { FederatedCommunityLoggingView } from '@logging/FederatedCommunityLogging.view' export async function startOpenConnectionCallback( args: OpenConnectionArgs, comA: DbCommunity, api: string, ): Promise { - logger.debug(`Authentication: startOpenConnectionCallback() with:`, args, comA) + logger.debug(`Authentication: startOpenConnectionCallback() with:`, { + args, + comA: new CommunityLoggingView(comA), + }) try { const homeFedCom = await DbFedCommunity.findOneByOrFail({ foreign: false, @@ -30,7 +35,10 @@ export async function startOpenConnectionCallback( // store oneTimeCode in requestedCom.community_uuid as authenticate-request-identifier comA.communityUuid = oneTimeCode.toString() await DbCommunity.save(comA) - logger.debug(`Authentication: stored oneTimeCode in requestedCom:`, comA) + logger.debug( + `Authentication: stored oneTimeCode in requestedCom:`, + new CommunityLoggingView(comA), + ) const client = AuthenticationClientFactory.getInstance(fedComA) // eslint-disable-next-line camelcase @@ -57,7 +65,10 @@ export async function startAuthentication( oneTimeCode: string, fedComB: DbFedCommunity, ): Promise { - logger.debug(`Authentication: startAuthentication()...`, oneTimeCode, fedComB) + logger.debug(`Authentication: startAuthentication()...`, { + oneTimeCode, + fedComB: new FederatedCommunityLoggingView(fedComB), + }) try { const homeCom = await DbCommunity.findOneByOrFail({ foreign: false }) @@ -78,7 +89,7 @@ export async function startAuthentication( logger.debug( `Authentication: received communityUUid for callbackFedCom:`, fedComUuid, - fedComB, + new FederatedCommunityLoggingView(fedComB), ) const callbackCom = await DbCommunity.findOneByOrFail({ foreign: true, @@ -88,7 +99,10 @@ export async function startAuthentication( callbackCom.communityUuid = fedComUuid callbackCom.authenticatedAt = new Date() await DbCommunity.save(callbackCom) - logger.debug('Authentication: Community Authentication successful:', callbackCom) + logger.debug( + 'Authentication: Community Authentication successful:', + new CommunityLoggingView(callbackCom), + ) } else { logger.error('Authentication: Community Authentication failed:', authenticationArgs) } diff --git a/federation/src/graphql/api/1_0/util/revertSettledReceiveTransaction.ts b/federation/src/graphql/api/1_0/util/revertSettledReceiveTransaction.ts index 4b1075cb2..bb5adec5c 100644 --- a/federation/src/graphql/api/1_0/util/revertSettledReceiveTransaction.ts +++ b/federation/src/graphql/api/1_0/util/revertSettledReceiveTransaction.ts @@ -15,6 +15,10 @@ import { federationLogger as logger } from '@/server/logger' import { getLastTransaction } from '@/graphql/util/getLastTransaction' import { TRANSACTIONS_LOCK } from '@/graphql/util/TRANSACTIONS_LOCK' +import { CommunityLoggingView } from '@logging/CommunityLogging.view' +import { UserLoggingView } from '@logging/UserLogging.view' +import { PendingTransactionLoggingView } from '@logging/PendingTransactionLogging.view' +import { TransactionLoggingView } from '@logging/TransactionLogging.view' export async function revertSettledReceiveTransaction( homeCom: DbCommunity, @@ -30,7 +34,11 @@ export async function revertSettledReceiveTransaction( logger.debug(`start Transaction for write-access...`) try { - logger.info('X-Com: revertSettledReceiveTransaction:', homeCom, receiverUser, pendingTx) + logger.info('X-Com: revertSettledReceiveTransaction:', { + homeCom: new CommunityLoggingView(homeCom), + receiverUser: new UserLoggingView(receiverUser), + pendingTx: new PendingTransactionLoggingView(pendingTx), + }) // ensure that no other pendingTx with the same sender or recipient exists const openSenderPendingTx = await DbPendingTransaction.count({ @@ -68,6 +76,7 @@ export async function revertSettledReceiveTransaction( pendingTx.balanceDate.toISOString(), ) logger.debug(`GradidoID:`, lastTransaction?.userGradidoID, pendingTx.userGradidoID) + // todo: Data privacy: personal user data in log file? logger.debug(`Name:`, lastTransaction?.userName, pendingTx.userName) logger.debug(`amount:`, lastTransaction?.amount.toString(), pendingTx.amount.toString()) logger.debug(`memo:`, lastTransaction?.memo, pendingTx.memo) @@ -90,7 +99,10 @@ export async function revertSettledReceiveTransaction( lastTransaction.linkedUserName === pendingTx.linkedUserName ) { await queryRunner.manager.remove(dbTransaction, lastTransaction) - logger.debug(`X-Com: revert settlement receive Transaction removed:`, lastTransaction) + logger.debug( + `X-Com: revert settlement receive Transaction removed:`, + new TransactionLoggingView(lastTransaction), + ) // and mark the pendingTx in the pending_transactions table as reverted pendingTx.state = PendingTransactionState.REVERTED await queryRunner.manager.save(DbPendingTransaction, pendingTx) @@ -98,12 +110,11 @@ export async function revertSettledReceiveTransaction( await queryRunner.commitTransaction() logger.debug(`commit revert settlement recipient Transaction successful...`) } else { - // TODO: if the last TX is not equivelant to pendingTX, the transactions must be corrected in EXPERT-MODE - throw new LogError( - `X-Com: missmatching transaction order for revert settlement!`, - lastTransaction, - pendingTx, - ) + // TODO: if the last TX is not equivalent to pendingTX, the transactions must be corrected in EXPERT-MODE + throw new LogError(`X-Com: mismatching transaction order for revert settlement!`, { + lastTransaction: lastTransaction ? new TransactionLoggingView(lastTransaction) : 'null', + pendingTx: new PendingTransactionLoggingView(pendingTx), + }) } /* diff --git a/federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts b/federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts index e73e7a5fd..0eadbe1c2 100644 --- a/federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts +++ b/federation/src/graphql/api/1_0/util/settlePendingReceiveTransaction.ts @@ -17,6 +17,10 @@ import { getLastTransaction } from '@/graphql/util/getLastTransaction' import { TRANSACTIONS_LOCK } from '@/graphql/util/TRANSACTIONS_LOCK' import { calculateRecipientBalance } from './calculateRecipientBalance' import Decimal from 'decimal.js-light' +import { CommunityLoggingView } from '@logging/CommunityLogging.view' +import { UserLoggingView } from '@logging/UserLogging.view' +import { PendingTransactionLoggingView } from '@logging/PendingTransactionLogging.view' +import { TransactionLoggingView } from '@logging/TransactionLogging.view' export async function settlePendingReceiveTransaction( homeCom: DbCommunity, @@ -32,7 +36,11 @@ export async function settlePendingReceiveTransaction( logger.debug(`start Transaction for write-access...`) try { - logger.info('X-Com: settlePendingReceiveTransaction:', homeCom, receiverUser, pendingTx) + logger.info('X-Com: settlePendingReceiveTransaction:', { + homeCom: new CommunityLoggingView(homeCom), + receiverUser: new UserLoggingView(receiverUser), + pendingTx: new PendingTransactionLoggingView(pendingTx), + }) // ensure that no other pendingTx with the same sender or recipient exists const openSenderPendingTx = await DbPendingTransaction.count({ @@ -84,7 +92,7 @@ export async function settlePendingReceiveTransaction( transactionReceive.previous = receiveBalance ? receiveBalance.lastTransactionId : null transactionReceive.linkedTransactionId = pendingTx.linkedTransactionId await queryRunner.manager.insert(dbTransaction, transactionReceive) - logger.debug(`receive Transaction inserted: ${dbTransaction}`) + logger.debug(`receive Transaction inserted: ${new TransactionLoggingView(transactionReceive)}`) // and mark the pendingTx in the pending_transactions table as settled pendingTx.state = PendingTransactionState.SETTLED diff --git a/federation/src/graphql/api/1_0/util/storeForeignUser.ts b/federation/src/graphql/api/1_0/util/storeForeignUser.ts index eeeb76a8f..861702d11 100644 --- a/federation/src/graphql/api/1_0/util/storeForeignUser.ts +++ b/federation/src/graphql/api/1_0/util/storeForeignUser.ts @@ -2,6 +2,8 @@ import { User as DbUser } from '@entity/User' import { federationLogger as logger } from '@/server/logger' import { SendCoinsArgs } from '../model/SendCoinsArgs' +import { UserLoggingView } from '@logging/UserLogging.view' +import { SendCoinsArgsLoggingView } from '../logger/SendCoinsArgsLogging.view' export async function storeForeignUser(args: SendCoinsArgs): Promise { if (args.senderCommunityUuid !== null && args.senderUserUuid !== null) { @@ -34,7 +36,7 @@ export async function storeForeignUser(args: SendCoinsArgs): Promise { } foreignUser.gradidoID = args.senderUserUuid foreignUser = await DbUser.save(foreignUser) - logger.debug('X-Com: new foreignUser inserted:', foreignUser) + logger.debug('X-Com: new foreignUser inserted:', new UserLoggingView(foreignUser)) return true } else if ( @@ -43,14 +45,13 @@ export async function storeForeignUser(args: SendCoinsArgs): Promise { args.senderUserName.slice(args.senderUserName.indexOf(' '), args.senderUserName.length) || user.alias !== args.senderAlias ) { - logger.warn( - 'X-Com: foreignUser still exists, but with different name or alias:', - user, - args, - ) + logger.warn('X-Com: foreignUser still exists, but with different name or alias:', { + user: new UserLoggingView(user), + args: new SendCoinsArgsLoggingView(args), + }) return false } else { - logger.debug('X-Com: foreignUser still exists...:', user) + logger.debug('X-Com: foreignUser still exists...:', new UserLoggingView(user)) return true } } catch (err) { diff --git a/federation/src/graphql/api/1_1/resolver/PublicKeyResolver.test.ts b/federation/src/graphql/api/1_1/resolver/PublicKeyResolver.test.ts index d41b53263..7ccec73af 100644 --- a/federation/src/graphql/api/1_1/resolver/PublicKeyResolver.test.ts +++ b/federation/src/graphql/api/1_1/resolver/PublicKeyResolver.test.ts @@ -39,7 +39,10 @@ describe('PublicKeyResolver', () => { homeCom.foreign = false homeCom.apiVersion = '1_0' homeCom.endPoint = 'endpoint-url' - homeCom.publicKey = Buffer.from('homeCommunity-publicKey') + homeCom.publicKey = Buffer.from( + '9f6dcd0d985cc7105cd71c3417d9c291b126c8ca90513197de02191f928ef713', + 'hex', + ) await DbFederatedCommunity.insert(homeCom) }) @@ -47,7 +50,7 @@ describe('PublicKeyResolver', () => { await expect(query({ query: getPublicKeyQuery })).resolves.toMatchObject({ data: { getPublicKey: { - publicKey: expect.stringMatching('homeCommunity-publicKey'), + publicKey: '9f6dcd0d985cc7105cd71c3417d9c291b126c8ca90513197de02191f928ef713', }, }, }) diff --git a/federation/src/server/createServer.ts b/federation/src/server/createServer.ts index b79847254..97729b882 100644 --- a/federation/src/server/createServer.ts +++ b/federation/src/server/createServer.ts @@ -24,6 +24,8 @@ import { Connection } from '@dbTools/typeorm' import { apolloLogger } from './logger' import { Logger } from 'log4js' +import helmet from 'helmet' +import { slowDown } from 'express-slow-down' // i18n // import { i18n } from './localization' @@ -62,6 +64,27 @@ export const createServer = async ( // cors app.use(cors) + // Helmet helps secure Express apps by setting HTTP response headers. + app.use(helmet()) + + // rate limiter/ slow down to many requests + const limiter = slowDown({ + windowMs: 1000, // 1 second + delayAfter: 10, // Allow 10 requests per 1 second. + delayMs: (hits) => hits * 50, // Add 100 ms of delay to every request after the 10th one. + /** + * So: + * + * - requests 1-10 are not delayed. + * - request 11 is delayed by 550ms + * - request 12 is delayed by 600ms + * - request 13 is delayed by 650ms + * + * and so on. After 1 seconds, the delay is reset to 0. + */ + }) + app.use(limiter) + // bodyparser json app.use(express.json()) // bodyparser urlencoded for elopage diff --git a/federation/tsconfig.json b/federation/tsconfig.json index 50ce9d0c7..0f96e80e5 100644 --- a/federation/tsconfig.json +++ b/federation/tsconfig.json @@ -60,7 +60,8 @@ /* external */ "@typeorm/*": ["../backend/src/typeorm/*", "../../backend/src/typeorm/*"], "@dbTools/*": ["../database/src/*", "../../database/build/src/*"], - "@entity/*": ["../database/entity/*", "../../database/build/entity/*"] + "@entity/*": ["../database/entity/*", "../../database/build/entity/*"], + "@logging/*": ["../database/logging/*", "../../database/build/logging/*"] }, // "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. */ diff --git a/federation/yarn.lock b/federation/yarn.lock index ca33138dd..74cc04521 100644 --- a/federation/yarn.lock +++ b/federation/yarn.lock @@ -2624,6 +2624,18 @@ expect@^27.5.1: jest-matcher-utils "^27.5.1" jest-message-util "^27.5.1" +express-rate-limit@7: + version "7.1.5" + resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-7.1.5.tgz#af4c81143a945ea97f2599d13957440a0ddbfcfe" + integrity sha512-/iVogxu7ueadrepw1bS0X0kaRC/U0afwiYRSLg68Ts+p4Dc85Q5QKsOnPS/QUjPMHvOJQtBDrZgvkOzf8ejUYw== + +express-slow-down@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/express-slow-down/-/express-slow-down-2.0.1.tgz#60c4515467314675d89c54ec608e2d586aa30f87" + integrity sha512-zRogSZhNXJYKDBekhgFfFXGrOngH7Fub7Mx2g8OQ4RUBwSJP/3TVEKMgSGR/WlneT0mJ6NBUnidHhIELGVPe3w== + dependencies: + express-rate-limit "7" + express@4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" @@ -3127,6 +3139,11 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +helmet@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-7.1.0.tgz#287279e00f8a3763d5dccbaf1e5ee39b8c3784ca" + integrity sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg== + html-encoding-sniffer@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" diff --git a/frontend/.env.dist b/frontend/.env.dist index 427d43359..f7e7edcd6 100644 --- a/frontend/.env.dist +++ b/frontend/.env.dist @@ -2,13 +2,13 @@ DEFAULT_PUBLISHER_ID=2896 # Endpoints -GRAPHQL_URI=http://localhost/graphql -ADMIN_AUTH_URL=http://localhost/admin/authenticate?token={token} +GRAPHQL_PATH=/graphql +ADMIN_AUTH_PATH=/admin/authenticate?token={token} # Community COMMUNITY_NAME=Gradido Entwicklung COMMUNITY_URL=http://localhost/ -COMMUNITY_REGISTER_URL=http://localhost/register +COMMUNITY_REGISTER_PATH=/register COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido. COMMUNITY_SUPPORT_MAIL=support@supportmail.com diff --git a/frontend/.env.template b/frontend/.env.template index 59e34eb80..c365ab8cf 100644 --- a/frontend/.env.template +++ b/frontend/.env.template @@ -4,18 +4,19 @@ CONFIG_VERSION=$FRONTEND_CONFIG_VERSION DEFAULT_PUBLISHER_ID=$DEFAULT_PUBLISHER_ID # Endpoints -GRAPHQL_URI=$GRAPHQL_URI -ADMIN_AUTH_URL=$ADMIN_AUTH_URL +GRAPHQL_PATH=$GRAPHQL_PATH +ADMIN_AUTH_PATH=$ADMIN_AUTH_PATH # Community COMMUNITY_NAME=$COMMUNITY_NAME -COMMUNITY_URL=$COMMUNITY_URL -COMMUNITY_REGISTER_URL=$COMMUNITY_REGISTER_URL +COMMUNITY_HOST=$COMMUNITY_HOST +URL_PROTOCOL=$URL_PROTOCOL +COMMUNITY_REGISTER_PATH=$COMMUNITY_REGISTER_PATH COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION COMMUNITY_SUPPORT_MAIL=$COMMUNITY_SUPPORT_MAIL # Meta -META_URL=$META_URL +META_URL=$COMMUNITY_HOST META_TITLE_DE=$META_TITLE_DE META_TITLE_EN=$META_TITLE_EN META_DESCRIPTION_DE=$META_DESCRIPTION_DE diff --git a/frontend/jest.config.js b/frontend/jest.config.js index 023fd64fd..4c3e6ab73 100644 --- a/frontend/jest.config.js +++ b/frontend/jest.config.js @@ -4,7 +4,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.{js,vue}', '!**/node_modules/**', '!**/?(*.)+(spec|test).js?(x)'], coverageThreshold: { global: { - lines: 95, + lines: 94, }, }, moduleFileExtensions: [ diff --git a/frontend/src/assets/News/news.json b/frontend/src/assets/News/news.json index 4f71a3460..45360631b 100644 --- a/frontend/src/assets/News/news.json +++ b/frontend/src/assets/News/news.json @@ -1,47 +1,37 @@ [ { "locale": "de", - "date": "30.10.2023", - "text": "Gradido wird dezentral (Beta)", - "url": "/send", - "extra": "Gradido-Communities können nun ihre eigenen dezentralen Gradido-Server betreiben. Transaktionen zwischen den Servern sind möglich. Im Senden-Dialog findest Du ein Auswahl-Menü mit den verfügbaren Communities, um das Ziel für den Empfänger auszuwählen. Bitte beachte, dass diese Funktion sich noch in der Beta-Testphase befindet und die Communities erst nach und nach hinzugefügt werden.", - "extra2": "Wenn Ihr als Community Euren eigenen Gradido-Server betreiben wollt, schreibt uns bitte eine E-Mail an ", - "email": "support@gradido.net" + "text": "Gradido-Kreise – Gemeinsam mit Freunden die Zukunft gestalten", + "button": "Mehr erfahren", + "url": "https://gradido.net/de/gradido-kreise-gemeinsam-mit-freunden-die-zukunft-gestalten", + "extra": "Ganz gleich, ob Ihr bereits einer Gruppe zugehörig seid oder ob Ihr Euch über Gradido gefunden habt – wenn Ihr gemeinsam Gradido nutzen wollt, braucht Ihr nicht gleich einen eigenen Gradido-Server." }, { "locale": "en", - "date": "30.10.2023", - "text": "Gradido becomes decentralized (Beta)", - "url": "/send", - "extra": "Gradido communities can now run their own decentralized Gradido servers. Transactions between the servers are possible. In the send dialog you will find a dropdown menu with the available communities to select the destination for the receiver. Please note that this feature is still in beta testing and communities will be added gradually.", - "extra2": "If you want to run your own Gradido server as a community, please send us an email to ", - "email": "support@gradido.net" + "text": "Gradido circles - Shaping the future together with friends", + "button": "Learn more", + "url": "https://gradido.net/en/gradido-kreise-gemeinsam-mit-freunden-die-zukunft-gestalten/", + "extra": "No matter whether you already belong to a group or whether you found each other via Gradido - if you want to use Gradido together, you don't need your own Gradido server." }, { "locale": "fr", - "date": "30.10.2023", - "text": "Gradido devient décentralisé (Beta)", - "url": "/send", - "extra": "Les communautés Gradido peuvent désormais gérer leurs propres serveurs Gradido décentralisés. Les transactions entre les serveurs sont possibles. Dans la boîte de dialogue d'envoi, tu trouveras un menu de sélection avec les communautés disponibles pour choisir la destination du destinataire. Veuillez noter que cette fonction est encore en phase de test bêta et que les communautés ne seront ajoutées qu'au fur et à mesure.", - "extra2": "Si vous souhaitez exploiter votre propre serveur Gradido en tant que communauté, veuillez nous envoyer un e-mail à ", - "email": "support@gradido.net" + "text": "Cercles Gradido - Construire l'avenir ensemble avec des amis ", + "button": "En savoir plus", + "url": "https://gradido.net/fr/gradido-kreise-gemeinsam-mit-freunden-die-zukunft-gestalten/", + "extra": "Que vous fassiez déjà partie d'un groupe ou que vous vous soyez trouvés par le biais de Gradido, si vous voulez utiliser Gradido ensemble, vous n'avez pas besoin de votre propre serveur Gradido." }, { "locale": "es", - "date": "30.10.2023", - "text": "Gradido se descentraliza (Beta)", - "url": "/send", - "extra": "Las comunidades de Gradido ya pueden gestionar sus propios servidores descentralizados de Gradido. Las transacciones entre los servidores son posibles. En el diálogo de envío encontrarás un menú desplegable con las comunidades disponibles para seleccionar el destino del destinatario. Ten en cuenta que esta función aún está en fase de pruebas beta y que las comunidades se irán añadiendo poco a poco.", - "extra2": "Si quieres gestionar tu propio servidor Gradido como comunidad, envíanos un correo electrónico a ", - "email": "support@gradido.net" + "text": "Círculos Gradido - Forjar el futuro entre amigos ", + "button": "Más información", + "url": "https://gradido.net/es/gradido-kreise-gemeinsam-mit-freunden-die-zukunft-gestalten/", + "extra": "No importa si ya pertenecéis a un grupo o si os habéis encontrado a través de Gradido: si queréis utilizar Gradido juntos, no necesitáis vuestro propio servidor Gradido." }, { "locale": "nl", - "date": "30.10.2023", - "text": "Gradido wordt gedecentraliseerd (Beta)", - "url": "/send", - "extra": "Gradido-gemeenschappen kunnen nu hun eigen gedecentraliseerde Gradido-servers beheren. Transacties tussen de servers zijn mogelijk. In het verzenddialoogvenster vind je een vervolgkeuzemenu met de beschikbare communities om de bestemming voor de ontvanger te selecteren. Houd er rekening mee dat deze functie zich nog in de beta-testfase bevindt en dat de communities beetje bij beetje zullen worden toegevoegd.", - "extra2": "Als je je eigen Gradido server als community wilt gebruiken, stuur ons dan een e-mail naar ", - "email": "support@gradido.net" + "text": "Gradidokringen - Samen met vrienden de toekomst vormgeven", + "button": "Meer informatie", + "url": "https://gradido.net/nl/gradido-kreise-gemeinsam-mit-freunden-die-zukunft-gestalten/", + "extra": "Het maakt niet uit of je al tot een groep behoort of dat je elkaar via Gradido hebt gevonden - als je Gradido samen wilt gebruiken, heb je geen eigen Gradido-server nodig." } ] diff --git a/frontend/src/components/CommunitySwitch.vue b/frontend/src/components/CommunitySwitch.vue index f9ecc804d..ac9ce3182 100644 --- a/frontend/src/components/CommunitySwitch.vue +++ b/frontend/src/components/CommunitySwitch.vue @@ -1,16 +1,23 @@