diff --git a/CHANGELOG.md b/CHANGELOG.md index a728f21ea..bbb49eaf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,28 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [2.2.0](https://github.com/gradido/gradido/compare/2.1.1...2.2.0) + +- feat(frontend): update news text [`#3279`](https://github.com/gradido/gradido/pull/3279) +- feat(frontend): use params instead of query for send/identifier route [`#3277`](https://github.com/gradido/gradido/pull/3277) +- fix(other): deployment bugfixes [`#3276`](https://github.com/gradido/gradido/pull/3276) +- refactor(other): federation optimize logging, fix bug [`#3272`](https://github.com/gradido/gradido/pull/3272) +- feat(other): request limit [`#3274`](https://github.com/gradido/gradido/pull/3274) +- feat(dlt): logging views [`#3270`](https://github.com/gradido/gradido/pull/3270) +- refactor(other): hetzner cloud deploy, refactor .env [`#3267`](https://github.com/gradido/gradido/pull/3267) +- refactor(dlt): dlt connector try out dci [`#3223`](https://github.com/gradido/gradido/pull/3223) +- feat(backend): fill linked_user_gradido_id and linked_user_name for creation transactions [`#3268`](https://github.com/gradido/gradido/pull/3268) +- feat(frontend): use day for contribution dates [`#3269`](https://github.com/gradido/gradido/pull/3269) +- feat(backend): fill linked_user_id for creation transactions [`#3266`](https://github.com/gradido/gradido/pull/3266) +- docs(other): 3258 feature create gms usecase docu [`#3260`](https://github.com/gradido/gradido/pull/3260) +- fix(frontend): update moderatorChangedMemo [`#3259`](https://github.com/gradido/gradido/pull/3259) +- chore(other): change filename date-pattern and stop all modules with db-write-access [`#3245`](https://github.com/gradido/gradido/pull/3245) + #### [2.1.1](https://github.com/gradido/gradido/compare/2.0.1...2.1.1) +> 1 December 2023 + +- chore(release): v2.1.1 [`#3257`](https://github.com/gradido/gradido/pull/3257) - feat(admin): wiedervorlage v2 [`#3255`](https://github.com/gradido/gradido/pull/3255) - feat(admin): resubmission [`#3252`](https://github.com/gradido/gradido/pull/3252) - feat(backend): grant moderator right to edit contribution memo [`#3233`](https://github.com/gradido/gradido/pull/3233) 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/package.json b/admin/package.json index 3c1e666e8..f80e20152 100644 --- a/admin/package.json +++ b/admin/package.json @@ -3,7 +3,7 @@ "description": "Administraion Interface for Gradido", "main": "index.js", "author": "Moriz Wahl", - "version": "2.1.1", + "version": "2.2.0", "license": "Apache-2.0", "private": false, "scripts": { 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..1cff23d5a 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,11 +49,11 @@ 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_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 EMAIL_CODE_REQUEST_TIME=$EMAIL_CODE_REQUEST_TIME @@ -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 32606c382..f7edec3dd 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -28,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..8f70ab11e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "gradido-backend", - "version": "2.1.1", + "version": "2.2.0", "description": "Gradido unified backend providing an API-Service for Gradido Transactions", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/backend", @@ -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/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 185a96bb6..2c4d2be1a 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -12,14 +12,14 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0082-join_community_federated_communities', + DB_VERSION: '0083-join_community_federated_communities', 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/UpdateUserInfosArgs.ts b/backend/src/graphql/arg/UpdateUserInfosArgs.ts index 6b2ab1032..0920fb3bc 100644 --- a/backend/src/graphql/arg/UpdateUserInfosArgs.ts +++ b/backend/src/graphql/arg/UpdateUserInfosArgs.ts @@ -1,6 +1,11 @@ import { IsBoolean, IsInt, IsString } from 'class-validator' -import { ArgsType, Field, Int } from 'type-graphql' +import { ArgsType, Field, InputType, Int } from 'type-graphql' +import { Location } from '@model/Location' + +import { isValidLocation } from '@/graphql/validator/Location' + +@InputType() @ArgsType() export class UpdateUserInfosArgs { @Field({ nullable: true }) @@ -38,4 +43,20 @@ export class UpdateUserInfosArgs { @Field({ nullable: true }) @IsBoolean() hideAmountGDT?: boolean + + @Field({ nullable: true, defaultValue: true }) + @IsBoolean() + gmsAllowed?: boolean + + @Field(() => Int, { nullable: true, defaultValue: 0 }) + @IsInt() + gmsPublishName?: number | null + + @Field(() => Location, { nullable: true }) + @isValidLocation() + gmsLocation?: Location | null + + @Field(() => Int, { nullable: true, defaultValue: 2 }) + @IsInt() + gmsPublishLocation?: number | null } 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/Location.ts b/backend/src/graphql/model/Location.ts new file mode 100644 index 000000000..2b4c720f4 --- /dev/null +++ b/backend/src/graphql/model/Location.ts @@ -0,0 +1,10 @@ +import { ArgsType, Field, Int } from 'type-graphql' + +@ArgsType() +export class Location { + @Field(() => Int) + longitude: number + + @Field(() => Int) + latitude: number +} 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 c3dbd4cda..58dc6a90c 100644 --- a/backend/src/graphql/resolver/CommunityResolver.test.ts +++ b/backend/src/graphql/resolver/CommunityResolver.test.ts @@ -51,6 +51,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 @@ -79,10 +133,7 @@ describe('CommunityResolver', () => { homeCom1 = DbFederatedCommunity.create() homeCom1.foreign = false - homeCom1.publicKey = Buffer.from( - '8a1f9374b99c30d827b85dcd23f7e50328430d64ef65ef35bf375ea8eb9a2e1d', - 'hex', - ) + homeCom1.publicKey = Buffer.from(ed25519KeyPairStaticHex[0].public, 'hex') homeCom1.apiVersion = '1_0' homeCom1.endPoint = 'http://localhost/api' homeCom1.createdAt = new Date() @@ -90,10 +141,7 @@ describe('CommunityResolver', () => { homeCom2 = DbFederatedCommunity.create() homeCom2.foreign = false - homeCom2.publicKey = Buffer.from( - '7d849b4b53232ed8f3c913e1944b4a20b1a33e44f65ee56673209c14df0e3252', - 'hex', - ) + homeCom2.publicKey = Buffer.from(ed25519KeyPairStaticHex[1].public, 'hex') homeCom2.apiVersion = '1_1' homeCom2.endPoint = 'http://localhost/api' homeCom2.createdAt = new Date() @@ -101,10 +149,7 @@ describe('CommunityResolver', () => { homeCom3 = DbFederatedCommunity.create() homeCom3.foreign = false - homeCom3.publicKey = Buffer.from( - 'f9a683abf2d1bf265c8dcb543d3674e080db5a5d6976a2dd6ea6969c798f8a8c', - 'hex', - ) + homeCom3.publicKey = Buffer.from(ed25519KeyPairStaticHex[2].public, 'hex') homeCom3.apiVersion = '2_0' homeCom3.endPoint = 'http://localhost/api' homeCom3.createdAt = new Date() @@ -118,10 +163,8 @@ describe('CommunityResolver', () => { { id: 3, foreign: homeCom3.foreign, - publicKey: expect.stringMatching( - 'f9a683abf2d1bf265c8dcb543d3674e080db5a5d6976a2dd6ea6969c798f8a8c', - ), - endPoint: expect.stringMatching('http://localhost/api/'), + publicKey: expect.stringMatching(ed25519KeyPairStaticHex[2].public), + url: expect.stringMatching('http://localhost/api/2_0'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -131,10 +174,8 @@ describe('CommunityResolver', () => { { id: 2, foreign: homeCom2.foreign, - publicKey: expect.stringMatching( - '7d849b4b53232ed8f3c913e1944b4a20b1a33e44f65ee56673209c14df0e3252', - ), - endPoint: expect.stringMatching('http://localhost/api/'), + publicKey: expect.stringMatching(ed25519KeyPairStaticHex[1].public), + url: expect.stringMatching('http://localhost/api/1_1'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -144,10 +185,8 @@ describe('CommunityResolver', () => { { id: 1, foreign: homeCom1.foreign, - publicKey: expect.stringMatching( - '8a1f9374b99c30d827b85dcd23f7e50328430d64ef65ef35bf375ea8eb9a2e1d', - ), - endPoint: expect.stringMatching('http://localhost/api/'), + publicKey: expect.stringMatching(ed25519KeyPairStaticHex[0].public), + url: expect.stringMatching('http://localhost/api/1_0'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -166,10 +205,7 @@ describe('CommunityResolver', () => { foreignCom1 = DbFederatedCommunity.create() foreignCom1.foreign = true - foreignCom1.publicKey = Buffer.from( - '2e3c58f8fb7d9e4c1f2e3a9ec51cf4b1473d9b6b422853ac4c28e68ec41db4f3', - 'hex', - ) + foreignCom1.publicKey = Buffer.from(ed25519KeyPairStaticHex[3].public, 'hex') foreignCom1.apiVersion = '1_0' foreignCom1.endPoint = 'http://remotehost/api' foreignCom1.createdAt = new Date() @@ -177,10 +213,7 @@ describe('CommunityResolver', () => { foreignCom2 = DbFederatedCommunity.create() foreignCom2.foreign = true - foreignCom2.publicKey = Buffer.from( - '9c74d200b9c8b7210db8e65a58b869e4a8c84eb5b2b8b8e8d1ed74e6d9f5b7a1', - 'hex', - ) + foreignCom2.publicKey = Buffer.from(ed25519KeyPairStaticHex[4].public, 'hex') foreignCom2.apiVersion = '1_1' foreignCom2.endPoint = 'http://remotehost/api' foreignCom2.createdAt = new Date() @@ -188,10 +221,7 @@ describe('CommunityResolver', () => { foreignCom3 = DbFederatedCommunity.create() foreignCom3.foreign = true - foreignCom3.publicKey = Buffer.from( - '13fd4edf5b708b5815f8d7d0c0ba34e6aa810f3f187f0bd3d25e0e74315ec16a', - 'hex', - ) + foreignCom3.publicKey = Buffer.from(ed25519KeyPairStaticHex[5].public, 'hex') foreignCom3.apiVersion = '1_2' foreignCom3.endPoint = 'http://remotehost/api' foreignCom3.createdAt = new Date() @@ -205,10 +235,8 @@ describe('CommunityResolver', () => { { id: 3, foreign: homeCom3.foreign, - publicKey: expect.stringMatching( - 'f9a683abf2d1bf265c8dcb543d3674e080db5a5d6976a2dd6ea6969c798f8a8c', - ), - endPoint: expect.stringMatching('http://localhost/api'), + publicKey: expect.stringMatching(ed25519KeyPairStaticHex[2].public), + url: expect.stringMatching('http://localhost/api/2_0'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -218,10 +246,8 @@ describe('CommunityResolver', () => { { id: 2, foreign: homeCom2.foreign, - publicKey: expect.stringMatching( - '7d849b4b53232ed8f3c913e1944b4a20b1a33e44f65ee56673209c14df0e3252', - ), - endPoint: expect.stringMatching('http://localhost/api'), + publicKey: expect.stringMatching(ed25519KeyPairStaticHex[1].public), + url: expect.stringMatching('http://localhost/api/1_1'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -231,10 +257,8 @@ describe('CommunityResolver', () => { { id: 1, foreign: homeCom1.foreign, - publicKey: expect.stringMatching( - '8a1f9374b99c30d827b85dcd23f7e50328430d64ef65ef35bf375ea8eb9a2e1d', - ), - endPoint: expect.stringMatching('http://localhost/api'), + publicKey: expect.stringMatching(ed25519KeyPairStaticHex[0].public), + url: expect.stringMatching('http://localhost/api/1_0'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -244,10 +268,8 @@ describe('CommunityResolver', () => { { id: 6, foreign: foreignCom3.foreign, - publicKey: expect.stringMatching( - '13fd4edf5b708b5815f8d7d0c0ba34e6aa810f3f187f0bd3d25e0e74315ec16a', - ), - endPoint: expect.stringMatching('http://remotehost/api'), + publicKey: expect.stringMatching(ed25519KeyPairStaticHex[5].public), + url: expect.stringMatching('http://remotehost/api/1_2'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -257,10 +279,8 @@ describe('CommunityResolver', () => { { id: 5, foreign: foreignCom2.foreign, - publicKey: expect.stringMatching( - '9c74d200b9c8b7210db8e65a58b869e4a8c84eb5b2b8b8e8d1ed74e6d9f5b7a1', - ), - endPoint: expect.stringMatching('http://remotehost/api'), + publicKey: expect.stringMatching(ed25519KeyPairStaticHex[4].public), + url: expect.stringMatching('http://remotehost/api/1_1'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -270,10 +290,8 @@ describe('CommunityResolver', () => { { id: 4, foreign: foreignCom1.foreign, - publicKey: expect.stringMatching( - '2e3c58f8fb7d9e4c1f2e3a9ec51cf4b1473d9b6b422853ac4c28e68ec41db4f3', - ), - endPoint: expect.stringMatching('http://remotehost/api'), + publicKey: expect.stringMatching(ed25519KeyPairStaticHex[3].public), + url: expect.stringMatching('http://remotehost/api/1_0'), lastAnnouncedAt: null, verifiedAt: null, lastErrorAt: null, @@ -317,11 +335,8 @@ describe('CommunityResolver', () => { homeCom1 = DbCommunity.create() homeCom1.foreign = false homeCom1.url = 'http://localhost/api' - homeCom1.publicKey = Buffer.from( - '8a1f9374b99c30d827b85dcd23f7e50328430d64ef65ef35bf375ea8eb9a2e1d', - 'hex', - ) - 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' @@ -358,11 +373,8 @@ describe('CommunityResolver', () => { homeCom1 = DbCommunity.create() homeCom1.foreign = false homeCom1.url = 'http://localhost/api' - homeCom1.publicKey = Buffer.from( - '8a1f9374b99c30d827b85dcd23f7e50328430d64ef65ef35bf375ea8eb9a2e1d', - 'hex', - ) - 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' @@ -373,14 +385,8 @@ describe('CommunityResolver', () => { foreignCom1 = DbCommunity.create() foreignCom1.foreign = true foreignCom1.url = 'http://stage-2.gradido.net/api' - foreignCom1.publicKey = Buffer.from( - '8a1f9374b99c30d827b85dcd23f7e50328430d64ef65ef35bf375ea8eb9a2e1d', - 'hex', - ) - foreignCom1.privateKey = Buffer.from( - 'f6c2a9d78e20a3c910f35b8ffcf824aa7b37f0d3d81bfc4c0e65e17a194b3a4a', - 'hex', - ) + 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' @@ -391,14 +397,8 @@ describe('CommunityResolver', () => { foreignCom2 = DbCommunity.create() foreignCom2.foreign = true foreignCom2.url = 'http://stage-3.gradido.net/api' - foreignCom2.publicKey = Buffer.from( - 'e047365a54082e8a7e9273da61b55c8134a2a0c836799ba12b78b9b0c52bc85f', - 'hex', - ) - foreignCom2.privateKey = Buffer.from( - 'e047365a54082e8a7e9273da61b55c8134a2a0c836799ba12b78b9b0c52bc85f', - 'hex', - ) + 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' @@ -506,7 +506,7 @@ describe('CommunityResolver', () => { }), ).resolves.toMatchObject({ data: { - getCommunityByUuid: { + community: { id: homeCom?.id, foreign: homeCom?.foreign, name: homeCom?.name, diff --git a/backend/src/graphql/resolver/CommunityResolver.ts b/backend/src/graphql/resolver/CommunityResolver.ts index e5bad3e45..d6e69694e 100644 --- a/backend/src/graphql/resolver/CommunityResolver.ts +++ b/backend/src/graphql/resolver/CommunityResolver.ts @@ -50,7 +50,7 @@ export class CommunityResolver { @Authorized([RIGHTS.COMMUNITY_BY_UUID]) @Query(() => Community) - async getCommunityByUuid(@Arg('communityUuid') communityUuid: string): Promise { + async community(@Arg('communityUuid') communityUuid: string): Promise { const com: DbCommunity | null = await getCommunityByUuid(communityUuid) if (!com) { throw new LogError('community not found', communityUuid) diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index d130a802e..1bb4a86f7 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -15,6 +15,8 @@ import { ApolloServerTestClient } from 'apollo-server-testing' import { GraphQLError } from 'graphql' import { v4 as uuidv4 } from 'uuid' +import { GmsPublishLocationType } from '@enum/GmsPublishLocationType' +import { GmsPublishNameType } from '@enum/GmsPublishNameType' import { cleanDB, testEnvironment } from '@test/helpers' import { logger } from '@test/testSetup' @@ -142,7 +144,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 +171,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 +214,7 @@ describe('send coins', () => { expect(logger.error).toBeCalledWith( 'No user with this credentials', 'garrick@ollivander.com', + homeCom.communityUuid, ) }) }) @@ -523,6 +534,9 @@ describe('send coins', () => { mutation: updateUserInfos, variables: { alias: 'bob', + gmsAllowed: true, + gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS, + gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM, }, }) await mutate({ diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 3261b313e..ce1ba43da 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -432,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..c570dbd9f 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -16,11 +16,14 @@ import { ApolloServerTestClient } from 'apollo-server-testing' import { GraphQLError } from 'graphql' import { v4 as uuidv4, validate as validateUUID, version as versionUUID } from 'uuid' +import { GmsPublishLocationType } from '@enum/GmsPublishLocationType' +import { GmsPublishNameType } from '@enum/GmsPublishNameType' import { OptInType } from '@enum/OptInType' import { PasswordEncryptionType } from '@enum/PasswordEncryptionType' import { RoleNames } from '@enum/RoleNames' import { UserContactType } from '@enum/UserContactType' import { ContributionLink } from '@model/ContributionLink' +import { Location } from '@model/Location' import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers' import { logger, i18n as localization } from '@test/testSetup' @@ -68,6 +71,8 @@ import { stephenHawking } from '@/seeds/users/stephen-hawking' import { printTimeDuration } from '@/util/time' import { objectValuesToArray } from '@/util/utilities' +import { Location2Point } from './util/Location2Point' + jest.mock('@/emails/sendEmailVariants', () => { const originalModule = jest.requireActual('@/emails/sendEmailVariants') return { @@ -1165,7 +1170,12 @@ describe('UserResolver', () => { it('throws an error', async () => { jest.clearAllMocks() resetToken() - await expect(mutate({ mutation: updateUserInfos })).resolves.toEqual( + await expect( + mutate({ + mutation: updateUserInfos, + variables: {}, + }), + ).resolves.toEqual( expect.objectContaining({ errors: [new GraphQLError('401 Unauthorized')], }), @@ -1190,7 +1200,12 @@ describe('UserResolver', () => { }) it('returns true', async () => { - await expect(mutate({ mutation: updateUserInfos })).resolves.toEqual( + await expect( + mutate({ + mutation: updateUserInfos, + variables: {}, + }), + ).resolves.toEqual( expect.objectContaining({ data: { updateUserInfos: true, @@ -1214,6 +1229,9 @@ describe('UserResolver', () => { firstName: 'Benjamin', lastName: 'Blümchen', language: 'en', + gmsAllowed: true, + gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS, + gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM, }), ]) }) @@ -1249,6 +1267,76 @@ describe('UserResolver', () => { await expect(User.find()).resolves.toEqual([ expect.objectContaining({ alias: 'bibi_Bloxberg', + gmsAllowed: true, + gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS, + gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM, + }), + ]) + }) + }) + }) + + describe('gms attributes', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('default settings', () => { + it('updates the user in DB', async () => { + await mutate({ + mutation: updateUserInfos, + variables: {}, + }) + await expect(User.find()).resolves.toEqual([ + expect.objectContaining({ + gmsAllowed: true, + gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS, + gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM, + }), + ]) + }) + }) + + describe('individual settings', () => { + it('updates the user in DB', async () => { + await mutate({ + mutation: updateUserInfos, + variables: { + gmsAllowed: false, + gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_FIRST_INITIAL, + gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_APPROXIMATE, + }, + }) + await expect(User.find()).resolves.toEqual([ + expect.objectContaining({ + gmsAllowed: false, + gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_FIRST_INITIAL, + gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_APPROXIMATE, + }), + ]) + }) + }) + + describe('with gms location', () => { + const loc = new Location() + loc.longitude = 9.573224 + loc.latitude = 49.679437 + it('updates the user in DB', async () => { + await mutate({ + mutation: updateUserInfos, + variables: { + gmsAllowed: true, + gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS, + gmsLocation: loc, + gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM, + }, + }) + await expect(User.find()).resolves.toEqual([ + expect.objectContaining({ + gmsAllowed: true, + gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS, + location: Location2Point(loc), + gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM, }), ]) }) @@ -2561,6 +2649,7 @@ describe('UserResolver', () => { query: userQuery, variables: { identifier: 'identifier', + communityIdentifier: 'community identifier', }, }), ).resolves.toEqual( @@ -2585,6 +2674,9 @@ describe('UserResolver', () => { mutation: updateUserInfos, variables: { alias: 'bibi', + gmsAllowed: true, + gmsPublishName: GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS, + gmsPublishLocation: GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM, }, }) }) @@ -2646,13 +2738,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..3f70ce112 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -19,11 +19,14 @@ import { SearchUsersFilters } from '@arg/SearchUsersFilters' import { SetUserRoleArgs } from '@arg/SetUserRoleArgs' import { UnsecureLoginArgs } from '@arg/UnsecureLoginArgs' import { UpdateUserInfosArgs } from '@arg/UpdateUserInfosArgs' +import { GmsPublishLocationType } from '@enum/GmsPublishLocationType' +import { GmsPublishNameType } from '@enum/GmsPublishNameType' import { OptInType } from '@enum/OptInType' import { Order } from '@enum/Order' import { PasswordEncryptionType } from '@enum/PasswordEncryptionType' import { UserContactType } from '@enum/UserContactType' import { SearchAdminUsersResult } from '@model/AdminUser' +// import { Location } from '@model/Location' import { User } from '@model/User' import { UserAdmin, SearchUsersResult } from '@model/UserAdmin' @@ -65,11 +68,12 @@ 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' import { getKlicktippState } from './util/getKlicktippState' +import { Location2Point } from './util/Location2Point' import { setUserRole, deleteUserRole } from './util/modifyUserRole' import { sendUserToGms } from './util/sendUserToGms' import { validateAlias } from './util/validateAlias' @@ -547,12 +551,29 @@ export class UserResolver { passwordNew, hideAmountGDD, hideAmountGDT, + gmsAllowed, + gmsPublishName, + gmsLocation, + gmsPublishLocation, }: UpdateUserInfosArgs, @Ctx() context: Context, ): Promise { - logger.info(`updateUserInfos(${firstName}, ${lastName}, ${language}, ***, ***)...`) - const user = getUser(context) + logger.info( + `updateUserInfos(${firstName}, ${lastName}, ${alias}, ${language}, ***, ***, ${hideAmountGDD}, ${hideAmountGDT}, ${gmsAllowed}, ${gmsPublishName}, ${gmsLocation}, ${gmsPublishLocation})...`, + ) + // check default arg settings + if (gmsAllowed === null || gmsAllowed === undefined) { + gmsAllowed = true + } + if (!gmsPublishName) { + gmsPublishName = GmsPublishNameType.GMS_PUBLISH_NAME_ALIAS_OR_INITALS + } + if (!gmsPublishLocation) { + gmsPublishLocation = GmsPublishLocationType.GMS_LOCATION_TYPE_RANDOM + } + const user = getUser(context) + // try { if (firstName) { user.firstName = firstName } @@ -599,6 +620,15 @@ export class UserResolver { user.hideAmountGDT = hideAmountGDT } + user.gmsAllowed = gmsAllowed + user.gmsPublishName = gmsPublishName + if (gmsLocation) { + user.location = Location2Point(gmsLocation) + } + user.gmsPublishLocation = gmsPublishLocation + // } catch (err) { + // console.log('error:', err) + // } const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() await queryRunner.startTransaction('REPEATABLE READ') @@ -609,7 +639,7 @@ export class UserResolver { }) await queryRunner.commitTransaction() - logger.debug('writing User data successful...') + logger.debug('writing User data successful...', user) } catch (e) { await queryRunner.rollbackTransaction() throw new LogError('Error on writing updated user data', e) @@ -834,11 +864,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/Location2Point.ts b/backend/src/graphql/resolver/util/Location2Point.ts new file mode 100644 index 000000000..df40c034a --- /dev/null +++ b/backend/src/graphql/resolver/util/Location2Point.ts @@ -0,0 +1,27 @@ +import { Point } from '@dbTools/typeorm' + +import { Location } from '@model/Location' + +export function Location2Point(location: Location): Point { + let pointStr: string + if (location.longitude && location.latitude) { + pointStr = '{ "type": "Point", "coordinates": [' + .concat(location.longitude?.toString()) + .concat(', ') + .concat(location.latitude?.toString()) + .concat('] }') + } else { + pointStr = '{ "type": "Point", "coordinates": [] }' + } + const point = JSON.parse(pointStr) as Point + return point +} + +export function Point2Location(point: Point): Location { + const location = new Location() + if (point.type === 'Point' && point.coordinates.length === 2) { + location.longitude = point.coordinates[0] + location.latitude = point.coordinates[1] + } + return location +} 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/scalar/Location.ts b/backend/src/graphql/scalar/Location.ts new file mode 100644 index 000000000..8b475d7f6 --- /dev/null +++ b/backend/src/graphql/scalar/Location.ts @@ -0,0 +1,43 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { GraphQLScalarType, Kind } from 'graphql' + +import { Location } from '@model/Location' + +import { LogError } from '@/server/LogError' + +export const LocationScalar = new GraphQLScalarType({ + name: 'Location', + description: + 'The `Location` scalar type to represent longitude and latitude values of a geo location', + + serialize(value: Location) { + return value + }, + + parseValue(value): Location { + try { + const loc = new Location() + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment + loc.longitude = value.longitude + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment + loc.latitude = value.latitude + return loc + } catch (err) { + throw new LogError('Error:', err) + } + // return new Location() + }, + + parseLiteral(ast): Location { + if (ast.kind !== Kind.STRING) { + throw new TypeError(`${String(ast)} is not a valid Location value.`) + } + let loc = new Location() + try { + loc = JSON.parse(ast.value) as Location + } catch (err) { + throw new LogError('Error:', err) + } + return loc + }, +}) diff --git a/backend/src/graphql/scalar/Point.ts b/backend/src/graphql/scalar/Point.ts new file mode 100644 index 000000000..06af56bfc --- /dev/null +++ b/backend/src/graphql/scalar/Point.ts @@ -0,0 +1,31 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +import { Point as DbPoint } from '@dbTools/typeorm' +import { GraphQLScalarType, Kind } from 'graphql' + +export const PointScalar = new GraphQLScalarType({ + name: 'Point', + description: + 'The `Point` scalar type to represent longitude and latitude values of a geo location', + + serialize(value: DbPoint) { + // Check type of value + if (value.type !== 'Point') { + throw new Error(`PointScalar can only serialize Geometry type 'Point' values`) + } + return value + }, + + parseValue(value): DbPoint { + const point = JSON.parse(value) as DbPoint + return point + }, + + parseLiteral(ast) { + if (ast.kind !== Kind.STRING) { + throw new TypeError(`${String(ast)} is not a valid Geometry value.`) + } + + const point = JSON.parse(ast.value) as DbPoint + return point + }, +}) diff --git a/backend/src/graphql/schema.ts b/backend/src/graphql/schema.ts index 18214861f..bcb8081a6 100644 --- a/backend/src/graphql/schema.ts +++ b/backend/src/graphql/schema.ts @@ -4,14 +4,20 @@ import { Decimal } from 'decimal.js-light' import { GraphQLSchema } from 'graphql' import { buildSchema } from 'type-graphql' +import { Location } from '@model/Location' + import { isAuthorized } from './directive/isAuthorized' import { DecimalScalar } from './scalar/Decimal' +import { LocationScalar } from './scalar/Location' export const schema = async (): Promise => { return buildSchema({ resolvers: [path.join(__dirname, 'resolver', `!(*.test).{js,ts}`)], authChecker: isAuthorized, - scalarsMap: [{ type: Decimal, scalar: DecimalScalar }], + scalarsMap: [ + { type: Decimal, scalar: DecimalScalar }, + { type: Location, scalar: LocationScalar }, + ], validate: { validationError: { target: false }, skipMissingProperties: true, diff --git a/backend/src/graphql/validator/Location.ts b/backend/src/graphql/validator/Location.ts new file mode 100644 index 000000000..f1f23cd81 --- /dev/null +++ b/backend/src/graphql/validator/Location.ts @@ -0,0 +1,29 @@ +import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator' + +import { Location } from '@model/Location' + +import { Location2Point } from '@/graphql/resolver/util/Location2Point' + +export function isValidLocation(validationOptions?: ValidationOptions) { + // eslint-disable-next-line @typescript-eslint/ban-types + return function (object: Object, propertyName: string) { + registerDecorator({ + name: 'isValidLocation', + target: object.constructor, + propertyName, + options: validationOptions, + validator: { + validate(value: Location) { + // console.log('isValidLocation:', value, value.getPoint()) + if (!value || Location2Point(value).type === 'Point') { + return true + } + return false + }, + defaultMessage(args: ValidationArguments) { + return `${propertyName} must be a valid Location, ${args.property}` + }, + }, + }) + } +} diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index 554cc4def..b10bb4b4e 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -34,6 +34,10 @@ export const updateUserInfos = gql` $locale: String $hideAmountGDD: Boolean $hideAmountGDT: Boolean + $gmsAllowed: Boolean + $gmsPublishName: Int + $gmsLocation: Location + $gmsPublishLocation: Int ) { updateUserInfos( firstName: $firstName @@ -44,6 +48,10 @@ export const updateUserInfos = gql` language: $locale hideAmountGDD: $hideAmountGDD hideAmountGDT: $hideAmountGDT + gmsAllowed: $gmsAllowed + gmsPublishName: $gmsPublishName + gmsLocation: $gmsLocation + gmsPublishLocation: $gmsPublishLocation ) } ` diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 05ef0c740..f988783b8 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -136,7 +136,7 @@ export const communitiesQuery = gql` export const getCommunityByUuidQuery = gql` query ($communityUuid: String!) { - getCommunityByUuid(communityUuid: $communityUuid) { + community(communityUuid: $communityUuid) { id foreign name @@ -387,7 +387,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..3f02b0afc 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,30 @@ 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) + // because of nginx proxy, needed for limiter + app.set('trust proxy', 1) + // bodyparser json app.use(json()) // bodyparser urlencoded for elopage diff --git a/backend/src/typeorm/connection.ts b/backend/src/typeorm/connection.ts index 3c8307478..104f6449d 100644 --- a/backend/src/typeorm/connection.ts +++ b/backend/src/typeorm/connection.ts @@ -30,6 +30,7 @@ export class Connection { Connection.instance = await createConnection({ name: 'default', type: 'mysql', + legacySpatialSupport: false, host: CONFIG.DB_HOST, port: CONFIG.DB_PORT, username: CONFIG.DB_USER, 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 7e329926b..28ddf1c38 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -58,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..91186187b 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" @@ -3471,6 +3483,11 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== +geojson@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/geojson/-/geojson-0.5.0.tgz#3cd6c96399be65b56ee55596116fe9191ce701c0" + integrity sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ== + get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -3679,18 +3696,20 @@ 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" crypto "^1.0.1" decimal.js-light "^2.5.1" dotenv "^10.0.0" + geojson "^0.5.0" mysql2 "^2.3.0" reflect-metadata "^0.1.13" ts-mysql-migrate "^1.0.2" typeorm "^0.3.16" uuid "^8.3.2" + wkx "^0.5.0" grapheme-splitter@^1.0.4: version "1.0.4" @@ -3826,6 +3845,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" @@ -7284,6 +7308,13 @@ with@^7.0.0: assert-never "^1.2.1" babel-walk "3.0.0-canary-5" +wkx@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.5.0.tgz#c6c37019acf40e517cc6b94657a25a3d4aa33e8c" + integrity sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg== + dependencies: + "@types/node" "*" + word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" 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/0081-introduce_gms_registration/Community.ts b/database/entity/0082-introduce_gms_registration/Community.ts similarity index 88% rename from database/entity/0081-introduce_gms_registration/Community.ts rename to database/entity/0082-introduce_gms_registration/Community.ts index 89c31763f..cc5607e2e 100644 --- a/database/entity/0081-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/0081-introduce_gms_registration/User.ts b/database/entity/0082-introduce_gms_registration/User.ts similarity index 89% rename from database/entity/0081-introduce_gms_registration/User.ts rename to database/entity/0082-introduce_gms_registration/User.ts index f91129f31..3dc0dccb6 100644 --- a/database/entity/0081-introduce_gms_registration/User.ts +++ b/database/entity/0082-introduce_gms_registration/User.ts @@ -8,11 +8,14 @@ import { JoinColumn, OneToOne, Geometry, + ManyToOne, } from 'typeorm' import { Contribution } from '../Contribution' import { ContributionMessage } from '../ContributionMessage' import { UserContact } from '../UserContact' import { UserRole } from '../UserRole' +import { GeometryTransformer } from '../../src/typeorm/GeometryTransformer' +import { Community } from '../Community' @Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) export class User extends BaseEntity { @@ -39,6 +42,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, @@ -125,7 +132,13 @@ export class User extends BaseEntity { @Column({ name: 'gms_allowed', type: 'bool', default: true }) gmsAllowed: boolean - @Column({ name: 'location', type: 'geometry', default: null, nullable: true }) + @Column({ + name: 'location', + type: 'geometry', + default: null, + nullable: true, + transformer: GeometryTransformer, + }) location: Geometry | null @Column({ diff --git a/database/entity/0081-introduce_gms_registration/UserContact.ts b/database/entity/0082-introduce_gms_registration/UserContact.ts similarity index 100% rename from database/entity/0081-introduce_gms_registration/UserContact.ts rename to database/entity/0082-introduce_gms_registration/UserContact.ts diff --git a/database/entity/0082-join_community_federated_communities/Community.ts b/database/entity/0083-join_community_federated_communities/Community.ts similarity index 91% rename from database/entity/0082-join_community_federated_communities/Community.ts rename to database/entity/0083-join_community_federated_communities/Community.ts index 5fdc9ccb5..839a0ee79 100644 --- a/database/entity/0082-join_community_federated_communities/Community.ts +++ b/database/entity/0083-join_community_federated_communities/Community.ts @@ -9,6 +9,7 @@ import { JoinColumn, } from 'typeorm' import { FederatedCommunity } from '../FederatedCommunity' +import { User } from '../User' @Entity('communities') export class Community extends BaseEntity { @@ -67,6 +68,10 @@ export class Community extends BaseEntity { }) updatedAt: Date | null + @OneToMany(() => User, (user) => user.community) + @JoinColumn({ name: 'community_uuid', referencedColumnName: 'communityUuid' }) + users: User[] + @OneToMany(() => FederatedCommunity, (federatedCommunity) => federatedCommunity.community) @JoinColumn({ name: 'public_key', referencedColumnName: 'publicKey' }) federatedCommunities?: FederatedCommunity[] diff --git a/database/entity/0082-join_community_federated_communities/FederatedCommunity.ts b/database/entity/0083-join_community_federated_communities/FederatedCommunity.ts similarity index 100% rename from database/entity/0082-join_community_federated_communities/FederatedCommunity.ts rename to database/entity/0083-join_community_federated_communities/FederatedCommunity.ts diff --git a/database/entity/Community.ts b/database/entity/Community.ts index 2a95c47f6..ccc752cd8 100644 --- a/database/entity/Community.ts +++ b/database/entity/Community.ts @@ -1 +1 @@ -export { Community } from './0082-join_community_federated_communities/Community' +export { Community } from './0083-join_community_federated_communities/Community' diff --git a/database/entity/FederatedCommunity.ts b/database/entity/FederatedCommunity.ts index 0f88dd844..044966080 100644 --- a/database/entity/FederatedCommunity.ts +++ b/database/entity/FederatedCommunity.ts @@ -1 +1 @@ -export { FederatedCommunity } from './0082-join_community_federated_communities/FederatedCommunity' +export { FederatedCommunity } from './0083-join_community_federated_communities/FederatedCommunity' diff --git a/database/entity/User.ts b/database/entity/User.ts index e5fc6527f..e3f15113d 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0081-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 c938f1312..e91e9a9d3 100644 --- a/database/entity/UserContact.ts +++ b/database/entity/UserContact.ts @@ -1 +1 @@ -export { UserContact } from './0081-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/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/0081-introduce_gms_registration.ts b/database/migrations/0082-introduce_gms_registration.ts similarity index 100% rename from database/migrations/0081-introduce_gms_registration.ts rename to database/migrations/0082-introduce_gms_registration.ts diff --git a/database/migrations/0082-join_community_federated_communities.ts b/database/migrations/0083-join_community_federated_communities.ts similarity index 100% rename from database/migrations/0082-join_community_federated_communities.ts rename to database/migrations/0083-join_community_federated_communities.ts diff --git a/database/package.json b/database/package.json index 8e1a99826..caeb917c4 100644 --- a/database/package.json +++ b/database/package.json @@ -1,6 +1,6 @@ { "name": "gradido-database", - "version": "2.1.1", + "version": "2.2.0", "description": "Gradido Database Tool to execute database migrations", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/database", @@ -21,6 +21,7 @@ "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^3.2.1", "@types/faker": "^5.5.9", + "@types/geojson": "^7946.0.13", "@types/node": "^16.10.3", "@typescript-eslint/eslint-plugin": "^5.57.1", "@typescript-eslint/parser": "^5.57.1", @@ -45,11 +46,13 @@ "crypto": "^1.0.1", "decimal.js-light": "^2.5.1", "dotenv": "^10.0.0", + "geojson": "^0.5.0", "mysql2": "^2.3.0", "reflect-metadata": "^0.1.13", "ts-mysql-migrate": "^1.0.2", "typeorm": "^0.3.16", - "uuid": "^8.3.2" + "uuid": "^8.3.2", + "wkx": "^0.5.0" }, "engines": { "node": ">=14" diff --git a/database/src/typeorm/GeometryTransformer.ts b/database/src/typeorm/GeometryTransformer.ts new file mode 100644 index 000000000..3598c493f --- /dev/null +++ b/database/src/typeorm/GeometryTransformer.ts @@ -0,0 +1,28 @@ +/* eslint-disable camelcase */ +import { Geometry as wkx_Geometry } from 'wkx' +import { Geometry } from 'geojson' +import { ValueTransformer } from 'typeorm/decorator/options/ValueTransformer' + +/** + * TypeORM transformer to convert GeoJSON to MySQL WKT (Well Known Text) e.g. POINT(LAT, LON) and back + */ +export const GeometryTransformer: ValueTransformer = { + to: (geojson: Geometry): string | null => { + if (geojson) { + const wkxg = wkx_Geometry.parseGeoJSON(geojson) + const str = wkxg.toWkt() + return str + } + return null + }, + + from: (wkb: string): Record | null => { + // wkb ? wkx_Geometry.parse(wkb).toGeoJSON() : undefined + if (!wkb) { + return null + } + const record = wkx_Geometry.parse(wkb) + const str = record.toGeoJSON() + return str + }, +} diff --git a/database/yarn.lock b/database/yarn.lock index d8a0d6ffb..fd5598693 100644 --- a/database/yarn.lock +++ b/database/yarn.lock @@ -143,6 +143,11 @@ resolved "https://registry.yarnpkg.com/@types/faker/-/faker-5.5.9.tgz#588ede92186dc557bff8341d294335d50d255f0c" integrity sha512-uCx6mP3UY5SIO14XlspxsGjgaemrxpssJI0Ol+GfhxtcKpv9pgRZYsS4eeKeHVLje6Qtc8lGszuBI461+gVZBA== +"@types/geojson@^7946.0.13": + version "7946.0.13" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.13.tgz#e6e77ea9ecf36564980a861e24e62a095988775e" + integrity sha512-bmrNrgKMOhM3WsafmbGmC+6dsF2Z308vLFsQ3a/bT8X8Sv5clVYpPars/UPq+sAaJP+5OoLAYgwbkS5QEJdLUQ== + "@types/json-schema@^7.0.9": version "7.0.12" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" @@ -1132,6 +1137,11 @@ generate-function@^2.3.1: dependencies: is-property "^1.0.2" +geojson@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/geojson/-/geojson-0.5.0.tgz#3cd6c96399be65b56ee55596116fe9191ce701c0" + integrity sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ== + get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -2502,6 +2512,13 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +wkx@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.5.0.tgz#c6c37019acf40e517cc6b94657a25a3d4aa33e8c" + integrity sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg== + dependencies: + "@types/node" "*" + word-wrap@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index aa6c6cade..9e6e911eb 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -1,87 +1,101 @@ -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,11 +104,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 +NGINX_REWRITE_LEGACY_URLS=false -WALLET_AUTH_URL=https://stage1.gradido.net/authenticate?token={token} -WALLET_URL=https://stage1.gradido.net/login +# LEGACY +DEFAULT_PUBLISHER_ID=2896 +WEBHOOK_ELOPAGE_SECRET=secret # GMS #GMS_ACTIVE=true 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..af8986318 100755 --- a/deployment/bare_metal/start.sh +++ b/deployment/bare_metal/start.sh @@ -1,5 +1,9 @@ #!/bin/bash - +# check for parameter +if [ -z "$1" ]; then + echo "Usage: Please provide a branch name as the first argument." + exit 1 +fi # Find current directory & configure paths set -o allexport SCRIPT_PATH=$(realpath $0) @@ -10,12 +14,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 +33,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 +48,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 +75,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 @@ -64,7 +84,7 @@ pm2 delete all pm2 save # git -BRANCH=${1:-master} +BRANCH=$1 echo "Starting with git pull - branch:$BRANCH" >> $UPDATE_HTML cd $PROJECT_ROOT # TODO: this overfetches alot, but ensures we can use start.sh with tags @@ -100,9 +120,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 +132,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 +197,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 +208,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 +218,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 +228,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 +239,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 +274,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..06b92ecaf --- /dev/null +++ b/deployment/hetzner_cloud/install.sh @@ -0,0 +1,166 @@ +#!/bin/bash +# check for parameter +if [ -z "$1" ]; then + echo "Usage: Please provide a branch name as the first argument." + exit 1 +fi + +# 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/package.json b/dht-node/package.json index ec47913de..7a73791d5 100644 --- a/dht-node/package.json +++ b/dht-node/package.json @@ -1,6 +1,6 @@ { "name": "gradido-dht-node", - "version": "2.1.1", + "version": "2.2.0", "description": "Gradido dht-node module", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/", diff --git a/dht-node/src/config/index.ts b/dht-node/src/config/index.ts index 73230ab6d..4e66aa5f9 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: '0082-join_community_federated_communities', + DB_VERSION: '0083-join_community_federated_communities', 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/jest.config.js b/dlt-connector/jest.config.js index 2de18cf50..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: 71, + lines: 66, }, }, setupFiles: ['/test/testSetup.ts'], diff --git a/dlt-connector/package.json b/dlt-connector/package.json index 5a4c54394..5bc9673de 100644 --- a/dlt-connector/package.json +++ b/dlt-connector/package.json @@ -1,6 +1,6 @@ { "name": "gradido-dlt-connector", - "version": "2.1.1", + "version": "2.2.0", "description": "Gradido DLT-Connector", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/", @@ -29,8 +29,10 @@ "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", 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 c129af2ea..e6febb482 100644 --- a/dlt-connector/src/config/index.ts +++ b/dlt-connector/src/config/index.ts @@ -6,7 +6,7 @@ const constants = { LOG4JS_CONFIG: 'log4js-config.json', 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/data/proto/3_3/GradidoTransaction.ts b/dlt-connector/src/data/proto/3_3/GradidoTransaction.ts index 4aaa3e25c..f38bcbd1f 100644 --- a/dlt-connector/src/data/proto/3_3/GradidoTransaction.ts +++ b/dlt-connector/src/data/proto/3_3/GradidoTransaction.ts @@ -1,5 +1,8 @@ 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' @@ -41,4 +44,16 @@ export class GradidoTransaction extends Message { } 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/graphql/resolver/CommunityResolver.ts b/dlt-connector/src/graphql/resolver/CommunityResolver.ts index d4bbeb28e..741de2e6d 100644 --- a/dlt-connector/src/graphql/resolver/CommunityResolver.ts +++ b/dlt-connector/src/graphql/resolver/CommunityResolver.ts @@ -9,8 +9,8 @@ import { TransactionResult } from '@model/TransactionResult' 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 { logger } from '@/server/logger' import { iotaTopicFromCommunityUUID } from '@/utils/typeConverter' @Resolver() diff --git a/dlt-connector/src/graphql/resolver/TransactionsResolver.ts b/dlt-connector/src/graphql/resolver/TransactionsResolver.ts index 10b55573e..6a5017fb1 100755 --- a/dlt-connector/src/graphql/resolver/TransactionsResolver.ts +++ b/dlt-connector/src/graphql/resolver/TransactionsResolver.ts @@ -4,6 +4,9 @@ import { TransactionDraft } from '@input/TransactionDraft' 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 { TransactionError } from '../model/TransactionError' @@ -35,8 +38,13 @@ export class TransactionResolver { } 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() } diff --git a/dlt-connector/src/graphql/schema.ts b/dlt-connector/src/graphql/schema.ts index 19a6d5566..bbd61c63f 100755 --- a/dlt-connector/src/graphql/schema.ts +++ b/dlt-connector/src/graphql/schema.ts @@ -10,7 +10,6 @@ export const schema = async (): Promise => { return buildSchema({ resolvers: [TransactionResolver, CommunityResolver], scalarsMap: [{ type: Decimal, scalar: DecimalScalar }], - emitSchemaFile: true, validate: { validationError: { target: false }, skipMissingProperties: true, diff --git a/dlt-connector/src/interactions/backendToDb/community/Community.role.ts b/dlt-connector/src/interactions/backendToDb/community/Community.role.ts index 30d91bfed..2b1514ef2 100644 --- a/dlt-connector/src/interactions/backendToDb/community/Community.role.ts +++ b/dlt-connector/src/interactions/backendToDb/community/Community.role.ts @@ -3,7 +3,8 @@ import { Community } from '@entity/Community' import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' import { CommunityDraft } from '@/graphql/input/CommunityDraft' import { TransactionError } from '@/graphql/model/TransactionError' -import { logger } from '@/server/logger' +import { CommunityLoggingView } from '@/logging/CommunityLogging.view' +import { logger } from '@/logging/logger' export abstract class CommunityRole { protected self: Community @@ -17,9 +18,11 @@ export abstract class CommunityRole { this.self.foreign = communityDraft.foreign } - public store(): Promise { + public async store(): Promise { try { - return this.self.save() + 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/HomeCommunity.role.ts b/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts index 256cfe1a5..7a4798368 100644 --- a/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts +++ b/dlt-connector/src/interactions/backendToDb/community/HomeCommunity.role.ts @@ -8,7 +8,8 @@ import { Mnemonic } from '@/data/Mnemonic' import { TransactionErrorType } from '@/graphql/enum/TransactionErrorType' import { CommunityDraft } from '@/graphql/input/CommunityDraft' import { TransactionError } from '@/graphql/model/TransactionError' -import { logger } from '@/server/logger' +import { CommunityLoggingView } from '@/logging/CommunityLogging.view' +import { logger } from '@/logging/logger' import { getDataSource } from '@/typeorm/DataSource' import { CreateTransactionRecipeContext } from '../transaction/CreateTransationRecipe.context' @@ -38,6 +39,7 @@ export class HomeCommunityRole extends CommunityRole { 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) { 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 100% rename from dlt-connector/src/server/logger.ts rename to dlt-connector/src/logging/logger.ts 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 e02cc3073..25e0a12e2 100755 --- a/dlt-connector/src/server/createServer.ts +++ b/dlt-connector/src/server/createServer.ts @@ -6,13 +6,14 @@ 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' -import { logger as dltLogger } from './logger' - type ServerDef = { apollo: ApolloServer; app: Express } interface MyContext { @@ -41,6 +42,29 @@ 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) + // because of nginx proxy, needed for limiter + app.set('trust proxy', 1) + await apollo.start() app.use( '/', diff --git a/dlt-connector/src/typeorm/DataSource.ts b/dlt-connector/src/typeorm/DataSource.ts index ecdfc1b66..a86a061f3 100644 --- a/dlt-connector/src/typeorm/DataSource.ts +++ b/dlt-connector/src/typeorm/DataSource.ts @@ -5,8 +5,8 @@ import { entities } from '@entity/index' import { Migration } from '@entity/Migration' import { CONFIG } from '@/config' +import { logger } from '@/logging/logger' import { LogError } from '@/server/LogError' -import { logger } from '@/server/logger' // eslint-disable-next-line @typescript-eslint/no-extraneous-class export class Connection { diff --git a/dlt-connector/src/utils/typeConverter.ts b/dlt-connector/src/utils/typeConverter.ts index 1fc46ee4b..52dcd2a98 100644 --- a/dlt-connector/src/utils/typeConverter.ts +++ b/dlt-connector/src/utils/typeConverter.ts @@ -7,8 +7,8 @@ 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' -import { logger } from '@/server/logger' export const uuid4ToBuffer = (uuid: string): Buffer => { // Remove dashes from the UUIDv4 string 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/yarn.lock b/dlt-connector/yarn.lock index 3c7a8bf36..6d50426b1 100644 --- a/dlt-connector/yarn.lock +++ b/dlt-connector/yarn.lock @@ -2833,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" @@ -3407,6 +3419,11 @@ hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: 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" 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..fcfaa0e46 100644 --- a/federation/package.json +++ b/federation/package.json @@ -1,6 +1,6 @@ { "name": "gradido-federation", - "version": "2.1.1", + "version": "2.2.0", "description": "Gradido federation module providing Gradido-Hub-Federation and versioned API for inter community communication", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/federation", @@ -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 fd87fce55..f8992a360 100644 --- a/federation/src/config/index.ts +++ b/federation/src/config/index.ts @@ -10,11 +10,11 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0082-join_community_federated_communities', + DB_VERSION: '0083-join_community_federated_communities', 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..3d3b80dd6 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,29 @@ 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) + // because of nginx proxy, needed for limiter + app.set('trust proxy', 1) + // 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/package.json b/frontend/package.json index fb5210a05..2a3eeb56a 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "bootstrap-vue-gradido-wallet", - "version": "2.1.1", + "version": "2.2.0", "private": true, "scripts": { "start": "node run/server.js", 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 @@