diff --git a/CHANGELOG.md b/CHANGELOG.md index d52ba760c..3e04d4c21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,49 @@ 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). +#### [1.21.0](https://github.com/gradido/gradido/compare/1.20.0...1.21.0) + +- feat(frontend): preserve email after login [`#2994`](https://github.com/gradido/gradido/pull/2994) +- feat(frontend): send coins via identifier [`#2989`](https://github.com/gradido/gradido/pull/2989) +- feat(backend): export user events to klicktipp [`#2916`](https://github.com/gradido/gradido/pull/2916) +- fix(backend): add extension pug json and css to nodemon. [`#2996`](https://github.com/gradido/gradido/pull/2996) +- feat(backend): send coins via alias [`#2988`](https://github.com/gradido/gradido/pull/2988) +- refactor(backend): replace jasonwebtoken with jose [`#2975`](https://github.com/gradido/gradido/pull/2975) +- feat(frontend): username in wallet [`#2984`](https://github.com/gradido/gradido/pull/2984) +- feat(frontend): add community to send form [`#2986`](https://github.com/gradido/gradido/pull/2986) +- fix(frontend): date fns locales [`#2983`](https://github.com/gradido/gradido/pull/2983) +- refactor(federation): federation reduce spam [`#2967`](https://github.com/gradido/gradido/pull/2967) +- refactor(federation): refactor federation clients [`#2965`](https://github.com/gradido/gradido/pull/2965) +- feat(backend): migrate transactions table for x community sendcoins [`#2917`](https://github.com/gradido/gradido/pull/2917) +- feat(backend): alias in update user info [`#2727`](https://github.com/gradido/gradido/pull/2727) +- refactor(backend): eslint comments [`#2981`](https://github.com/gradido/gradido/pull/2981) +- refactor(backend): eslint security [`#2980`](https://github.com/gradido/gradido/pull/2980) +- refactor(backend): rename klicktippSignIn to subscribe. [`#2973`](https://github.com/gradido/gradido/pull/2973) +- refactor(backend): eslint typescript strict [`#2979`](https://github.com/gradido/gradido/pull/2979) +- fix(frontend): between store problems [`#2972`](https://github.com/gradido/gradido/pull/2972) +- refactor(other): delete build folders [`#2977`](https://github.com/gradido/gradido/pull/2977) +- refactor(backend): no email in user [`#2953`](https://github.com/gradido/gradido/pull/2953) +- refactor(frontend): remove email in wallet [`#2952`](https://github.com/gradido/gradido/pull/2952) +- fix(frontend): update jest-canvas-mock version to resolve window mock problem in tests [`#2974`](https://github.com/gradido/gradido/pull/2974) +- feat(federation): federation autoreload on codechange [`#2969`](https://github.com/gradido/gradido/pull/2969) +- feat(backend): add fields to subscriber [`#2887`](https://github.com/gradido/gradido/pull/2887) +- feat(backend): x-com-2: distingue communities and communities_federation in database [`#2890`](https://github.com/gradido/gradido/pull/2890) +- feat(backend): add event for subscribe and unsubscribe [`#2886`](https://github.com/gradido/gradido/pull/2886) +- refactor(backend): eslint disable more typesafety [`#2922`](https://github.com/gradido/gradido/pull/2922) +- refactor(backend): eslint disable tests typesafer [`#2921`](https://github.com/gradido/gradido/pull/2921) +- refactor(backend): eslint disable @typescript eslint/unbound method [`#2920`](https://github.com/gradido/gradido/pull/2920) +- docs(other): removed obsolete yarn cron docu [`#2909`](https://github.com/gradido/gradido/pull/2909) +- refactor(other): finalize workflow separation and resolve mariadb and database dependencies in workflow files [`#2962`](https://github.com/gradido/gradido/pull/2962) +- refactor(workflow): align workflow naming and remove docker-compose filter from build tests [`#2894`](https://github.com/gradido/gradido/pull/2894) +- refactor(backend): eslint plugin promise + fixes [`#2830`](https://github.com/gradido/gradido/pull/2830) +- fix(backend): log stack trace included [`#2915`](https://github.com/gradido/gradido/pull/2915) +- refactor(backend): prettier refine config [`#2832`](https://github.com/gradido/gradido/pull/2832) + #### [1.20.0](https://github.com/gradido/gradido/compare/1.19.1...1.20.0) +> 12 April 2023 + +- chore(release): v1.20.0 [`#2939`](https://github.com/gradido/gradido/pull/2939) - fix(backend): no await for emails [`#2918`](https://github.com/gradido/gradido/pull/2918) - fix(frontend): no receiver on send by link [`#2933`](https://github.com/gradido/gradido/pull/2933) - fix(admin): pagination set currentPage by switch tabs [`#2902`](https://github.com/gradido/gradido/pull/2902) diff --git a/admin/package.json b/admin/package.json index 7a3b36a7a..823201b7d 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": "1.20.0", + "version": "1.21.0", "license": "Apache-2.0", "private": false, "scripts": { diff --git a/backend/.eslintrc.js b/backend/.eslintrc.js index 798bef1e6..0007fd125 100644 --- a/backend/.eslintrc.js +++ b/backend/.eslintrc.js @@ -27,7 +27,8 @@ module.exports = { }, }, rules: { - 'no-console': ['error'], + 'no-console': 'error', + camelcase: ['error', { allow: ['FederationClient_*'] }], 'no-debugger': 'error', 'prettier/prettier': [ 'error', @@ -57,7 +58,7 @@ module.exports = { 'import/no-dynamic-require': 'error', 'import/no-internal-modules': 'off', 'import/no-relative-packages': 'error', - 'import/no-relative-parent-imports': ['error', { ignore: ['@/*'] }], + 'import/no-relative-parent-imports': ['error', { ignore: ['@/*', 'random-bigint'] }], 'import/no-self-import': 'error', 'import/no-unresolved': 'error', 'import/no-useless-path-segments': 'error', @@ -184,6 +185,7 @@ module.exports = { tsconfigRootDir: __dirname, project: ['./tsconfig.json', '**/tsconfig.json'], // this is to properly reference the referenced project database without requirement of compiling it + // eslint-disable-next-line camelcase EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true, }, }, diff --git a/backend/@types/random-bigint/index.d.ts b/backend/@types/random-bigint/index.d.ts new file mode 100644 index 000000000..0f685e722 --- /dev/null +++ b/backend/@types/random-bigint/index.d.ts @@ -0,0 +1,5 @@ +/* eslint-disable @typescript-eslint/ban-types */ +declare module 'random-bigint' { + function random(bits: number, cb?: (err: Error, num: BigInt) => void): BigInt + export = random +} diff --git a/backend/package.json b/backend/package.json index dce2b5e6b..68ec89ec0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "gradido-backend", - "version": "1.20.0", + "version": "1.21.0", "description": "Gradido unified backend providing an API-Service for Gradido Transactions", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/backend", @@ -15,7 +15,7 @@ "lint": "eslint --max-warnings=0 .", "test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --forceExit --detectOpenHandles", "seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts", - "klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/klicktipp.ts", + "klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/executeKlicktipp.ts", "locales": "scripts/sort.sh" }, "dependencies": { diff --git a/backend/src/apis/KlicktippController.ts b/backend/src/apis/KlicktippController.ts index a2a8f86cb..ea6a8e47c 100644 --- a/backend/src/apis/KlicktippController.ts +++ b/backend/src/apis/KlicktippController.ts @@ -4,8 +4,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ - import { CONFIG } from '@/config' +import { backendLogger as logger } from '@/server/logger' // eslint-disable-next-line import/no-relative-parent-imports import KlicktippConnector from 'klicktipp-api' @@ -41,9 +41,12 @@ export const getKlickTippUser = async (email: string): Promise => { if (!CONFIG.KLICKTIPP) return true const isLogin = await loginKlicktippUser() if (isLogin) { - const subscriberId = await klicktippConnector.subscriberSearch(email) - const result = await klicktippConnector.subscriberGet(subscriberId) - return result + try { + return klicktippConnector.subscriberGet(await klicktippConnector.subscriberSearch(email)) + } catch (e) { + logger.error('Could not find subscriber', email) + return false + } } return false } @@ -62,8 +65,18 @@ export const addFieldsToSubscriber = async ( if (!CONFIG.KLICKTIPP) return true const isLogin = await loginKlicktippUser() if (isLogin) { - const subscriberId = await klicktippConnector.subscriberSearch(email) - return klicktippConnector.subscriberUpdate(subscriberId, fields, newemail, newsmsnumber) + try { + logger.info('Updating of subscriber', email) + return klicktippConnector.subscriberUpdate( + await klicktippConnector.subscriberSearch(email), + fields, + newemail, + newsmsnumber, + ) + } catch (e) { + logger.error('Could not update subscriber', email, fields, e) + return false + } } return false } diff --git a/backend/src/federation/client/Client_1_0.ts b/backend/src/federation/client/1_0/FederationClient.ts similarity index 94% rename from backend/src/federation/client/Client_1_0.ts rename to backend/src/federation/client/1_0/FederationClient.ts index 0c0d458c8..ba446abe8 100644 --- a/backend/src/federation/client/Client_1_0.ts +++ b/backend/src/federation/client/1_0/FederationClient.ts @@ -1,11 +1,11 @@ import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { GraphQLClient } from 'graphql-request' -import { getPublicKey } from '@/federation/query/getPublicKey' +import { getPublicKey } from '@/federation/client/1_0/query/getPublicKey' import { backendLogger as logger } from '@/server/logger' // eslint-disable-next-line camelcase -export class Client_1_0 { +export class FederationClient { dbCom: DbFederatedCommunity endpoint: string client: GraphQLClient diff --git a/backend/src/federation/query/getPublicKey.ts b/backend/src/federation/client/1_0/query/getPublicKey.ts similarity index 100% rename from backend/src/federation/query/getPublicKey.ts rename to backend/src/federation/client/1_0/query/getPublicKey.ts diff --git a/backend/src/federation/client/1_1/FederationClient.ts b/backend/src/federation/client/1_1/FederationClient.ts new file mode 100644 index 000000000..2fdfedd92 --- /dev/null +++ b/backend/src/federation/client/1_1/FederationClient.ts @@ -0,0 +1,5 @@ +// eslint-disable-next-line camelcase +import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient' + +// eslint-disable-next-line camelcase +export class FederationClient extends V1_0_FederationClient {} diff --git a/backend/src/federation/client/Client_1_1.ts b/backend/src/federation/client/Client_1_1.ts deleted file mode 100644 index 8525acc5d..000000000 --- a/backend/src/federation/client/Client_1_1.ts +++ /dev/null @@ -1,5 +0,0 @@ -// eslint-disable-next-line camelcase -import { Client_1_0 } from './Client_1_0' - -// eslint-disable-next-line camelcase -export class Client_1_1 extends Client_1_0 {} diff --git a/backend/src/federation/client/Client.ts b/backend/src/federation/client/FederationClientFactory.ts similarity index 61% rename from backend/src/federation/client/Client.ts rename to backend/src/federation/client/FederationClientFactory.ts index 98f63c127..d057ffd04 100644 --- a/backend/src/federation/client/Client.ts +++ b/backend/src/federation/client/FederationClientFactory.ts @@ -1,24 +1,23 @@ import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' +// eslint-disable-next-line camelcase +import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient' +// eslint-disable-next-line camelcase +import { FederationClient as V1_1_FederationClient } from '@/federation/client/1_1/FederationClient' import { ApiVersionType } from '@/federation/enum/apiVersionType' // eslint-disable-next-line camelcase -import { Client_1_0 } from './Client_1_0' -// eslint-disable-next-line camelcase -import { Client_1_1 } from './Client_1_1' +type FederationClient = V1_0_FederationClient | V1_1_FederationClient -// eslint-disable-next-line camelcase -type FederationClient = Client_1_0 | Client_1_1 - -interface ClientInstance { +interface FederationClientInstance { id: number // eslint-disable-next-line no-use-before-define client: FederationClient } // eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class Client { - private static instanceArray: ClientInstance[] = [] +export class FederationClientFactory { + private static instanceArray: FederationClientInstance[] = [] /** * The Singleton's constructor should always be private to prevent direct @@ -30,9 +29,9 @@ export class Client { private static createFederationClient = (dbCom: DbFederatedCommunity) => { switch (dbCom.apiVersion) { case ApiVersionType.V1_0: - return new Client_1_0(dbCom) + return new V1_0_FederationClient(dbCom) case ApiVersionType.V1_1: - return new Client_1_1(dbCom) + return new V1_1_FederationClient(dbCom) default: return null } @@ -45,13 +44,18 @@ export class Client { * just one instance of each subclass around. */ public static getInstance(dbCom: DbFederatedCommunity): FederationClient | null { - const instance = Client.instanceArray.find((instance) => instance.id === dbCom.id) + const instance = FederationClientFactory.instanceArray.find( + (instance) => instance.id === dbCom.id, + ) if (instance) { return instance.client } - const client = Client.createFederationClient(dbCom) + const client = FederationClientFactory.createFederationClient(dbCom) if (client) { - Client.instanceArray.push({ id: dbCom.id, client } as ClientInstance) + 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 77d0cc2ad..834f37e16 100644 --- a/backend/src/federation/validateCommunities.test.ts +++ b/backend/src/federation/validateCommunities.test.ts @@ -8,6 +8,8 @@ import { Connection } from '@dbTools/typeorm' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { ApolloServerTestClient } from 'apollo-server-testing' +import { GraphQLClient } from 'graphql-request' +import { Response } from 'graphql-request/dist/types' import { testEnvironment, cleanDB } from '@test/helpers' import { logger } from '@test/testSetup' @@ -57,10 +59,23 @@ describe('validate Communities', () => { expect(logger.debug).toBeCalledWith(`Federation: found 0 dbCommunities`) }) - describe('with one Community of api 1_0', () => { + describe('with one Community of api 1_0 and not matching pubKey', () => { beforeEach(async () => { + // eslint-disable-next-line @typescript-eslint/require-await + jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + data: { + getPublicKey: { + publicKey: 'somePubKey', + }, + }, + } as Response + }) const variables1 = { - publicKey: Buffer.from('11111111111111111111111111111111'), + publicKey: Buffer.from( + '1111111111111111111111111111111111111111111111111111111111111111', + ), apiVersion: '1_0', endPoint: 'http//localhost:5001/api/', lastAnnouncedAt: new Date(), @@ -70,6 +85,7 @@ describe('validate Communities', () => { .into(DbFederatedCommunity) .values(variables1) .orUpdate({ + // eslint-disable-next-line camelcase conflict_target: ['id', 'publicKey', 'apiVersion'], overwrite: ['end_point', 'last_announced_at'], }) @@ -88,11 +104,85 @@ describe('validate Communities', () => { 'http//localhost:5001/api/1_0/', ) }) + it('logs not matching publicKeys', () => { + expect(logger.warn).toBeCalledWith( + 'Federation: received not matching publicKey:', + 'somePubKey', + expect.stringMatching('1111111111111111111111111111111111111111111111111111111111111111'), + ) + }) + }) + describe('with one Community of api 1_0 and matching pubKey', () => { + beforeEach(async () => { + // eslint-disable-next-line @typescript-eslint/require-await + jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + data: { + getPublicKey: { + publicKey: '1111111111111111111111111111111111111111111111111111111111111111', + }, + }, + } as Response + }) + const variables1 = { + publicKey: Buffer.from( + '1111111111111111111111111111111111111111111111111111111111111111', + ), + apiVersion: '1_0', + endPoint: 'http//localhost:5001/api/', + lastAnnouncedAt: new Date(), + } + await DbFederatedCommunity.createQueryBuilder() + .insert() + .into(DbFederatedCommunity) + .values(variables1) + .orUpdate({ + // eslint-disable-next-line camelcase + conflict_target: ['id', 'publicKey', 'apiVersion'], + overwrite: ['end_point', 'last_announced_at'], + }) + .execute() + await DbFederatedCommunity.update({}, { verifiedAt: null }) + jest.clearAllMocks() + await validateCommunities() + }) + + it('logs one community found', () => { + expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`) + }) + it('logs requestGetPublicKey for community api 1_0 ', () => { + expect(logger.info).toBeCalledWith( + 'Federation: getPublicKey from endpoint', + 'http//localhost:5001/api/1_0/', + ) + }) + it('logs community pubKey verified', () => { + expect(logger.info).toHaveBeenNthCalledWith( + 3, + 'Federation: verified community with', + 'http//localhost:5001/api/', + ) + }) }) describe('with two Communities of api 1_0 and 1_1', () => { beforeEach(async () => { + jest.clearAllMocks() + // eslint-disable-next-line @typescript-eslint/require-await + jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + data: { + getPublicKey: { + publicKey: '1111111111111111111111111111111111111111111111111111111111111111', + }, + }, + } as Response + }) const variables2 = { - publicKey: Buffer.from('11111111111111111111111111111111'), + publicKey: Buffer.from( + '1111111111111111111111111111111111111111111111111111111111111111', + ), apiVersion: '1_1', endPoint: 'http//localhost:5001/api/', lastAnnouncedAt: new Date(), @@ -102,11 +192,13 @@ describe('validate Communities', () => { .into(DbFederatedCommunity) .values(variables2) .orUpdate({ + // eslint-disable-next-line camelcase conflict_target: ['id', 'publicKey', 'apiVersion'], overwrite: ['end_point', 'last_announced_at'], }) .execute() + await DbFederatedCommunity.update({}, { verifiedAt: null }) jest.clearAllMocks() await validateCommunities() }) @@ -130,7 +222,9 @@ describe('validate Communities', () => { let dbCom: DbFederatedCommunity beforeEach(async () => { const variables3 = { - publicKey: Buffer.from('11111111111111111111111111111111'), + publicKey: Buffer.from( + '1111111111111111111111111111111111111111111111111111111111111111', + ), apiVersion: '2_0', endPoint: 'http//localhost:5001/api/', lastAnnouncedAt: new Date(), @@ -140,6 +234,7 @@ describe('validate Communities', () => { .into(DbFederatedCommunity) .values(variables3) .orUpdate({ + // eslint-disable-next-line camelcase conflict_target: ['id', 'publicKey', 'apiVersion'], overwrite: ['end_point', 'last_announced_at'], }) @@ -147,6 +242,7 @@ describe('validate Communities', () => { dbCom = await DbFederatedCommunity.findOneOrFail({ where: { publicKey: variables3.publicKey, apiVersion: variables3.apiVersion }, }) + await DbFederatedCommunity.update({}, { verifiedAt: null }) jest.clearAllMocks() await validateCommunities() }) diff --git a/backend/src/federation/validateCommunities.ts b/backend/src/federation/validateCommunities.ts index 4b337eda9..91c6ee724 100644 --- a/backend/src/federation/validateCommunities.ts +++ b/backend/src/federation/validateCommunities.ts @@ -3,9 +3,11 @@ import { IsNull } from '@dbTools/typeorm' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' +// eslint-disable-next-line camelcase +import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient' +import { FederationClientFactory } from '@/federation/client/FederationClientFactory' import { backendLogger as logger } from '@/server/logger' -import { Client } from './client/Client' import { ApiVersionType } from './enum/apiVersionType' export function startValidateCommunities(timerInterval: number): void { @@ -37,17 +39,20 @@ export async function validateCommunities(): Promise { continue } try { - const client = Client.getInstance(dbCom) - const pubKey = await client?.getPublicKey() - if (pubKey && pubKey === dbCom.publicKey.toString()) { - await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() }) - logger.info('Federation: verified community', dbCom) - } else { - logger.warn( - 'Federation: received not matching publicKey:', - pubKey, - dbCom.publicKey.toString(), - ) + const client = FederationClientFactory.getInstance(dbCom) + // eslint-disable-next-line camelcase + if (client instanceof V1_0_FederationClient) { + const pubKey = await client.getPublicKey() + if (pubKey && pubKey === dbCom.publicKey.toString()) { + await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() }) + logger.info('Federation: verified community with', dbCom.endPoint) + } else { + logger.warn( + 'Federation: received not matching publicKey:', + pubKey, + dbCom.publicKey.toString(), + ) + } } } catch (err) { logger.error(`Error:`, err) diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 04f636b5a..40e9ec2dd 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -322,8 +322,6 @@ export class TransactionResolver { throw new LogError('Amount to send must be positive', amount) } - // TODO this is subject to replay attacks - // --- WHY? const senderUser = getUser(context) // validate recipient user diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index cbfd9b5c5..9934c93de 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -70,13 +70,13 @@ import { communityDbUser } from '@/util/communityUser' import { hasElopageBuys } from '@/util/hasElopageBuys' import { getTimeDurationObject, printTimeDuration } from '@/util/time' +import random from 'random-bigint' + import { FULL_CREATION_AVAILABLE } from './const/const' import { getUserCreations } from './util/creations' import { findUserByIdentifier } from './util/findUserByIdentifier' import { validateAlias } from './util/validateAlias' -// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs -const random = require('random-bigint') // eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs const sodium = require('sodium-native') diff --git a/backend/src/graphql/resolver/util/eventList.ts b/backend/src/graphql/resolver/util/eventList.ts new file mode 100644 index 000000000..45afe1832 --- /dev/null +++ b/backend/src/graphql/resolver/util/eventList.ts @@ -0,0 +1,17 @@ +import { Event as DbEvent } from '@entity/Event' +import { User } from '@entity/User' +import { UserContact } from '@entity/UserContact' + +export const lastDateTimeEvents = async ( + eventType: string, +): Promise<{ email: string; value: Date }[]> => { + return DbEvent.createQueryBuilder('event') + .select('MAX(event.created_at)', 'value') + .leftJoin(User, 'user', 'affected_user_id = user.id') + .leftJoin(UserContact, 'usercontact', 'user.id = usercontact.user_id') + .addSelect('usercontact.email', 'email') + .where('event.type = :eventType', { eventType }) + .andWhere('usercontact.email IS NOT NULL') + .groupBy('event.affected_user_id') + .getRawMany() +} diff --git a/backend/src/server/createServer.ts b/backend/src/server/createServer.ts index d813c541e..c162d9f6f 100644 --- a/backend/src/server/createServer.ts +++ b/backend/src/server/createServer.ts @@ -1,14 +1,14 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/unbound-method */ -import { Connection } from '@dbTools/typeorm' +import { Connection as DbConnection } from '@dbTools/typeorm' import { ApolloServer } from 'apollo-server-express' import express, { Express, json, urlencoded } from 'express' import { Logger } from 'log4js' import { CONFIG } from '@/config' import { schema } from '@/graphql/schema' -import { connection } from '@/typeorm/connection' +import { Connection } from '@/typeorm/connection' import { checkDBVersion } from '@/typeorm/DBVersion' import { elopageWebhook } from '@/webhook/elopage' @@ -24,7 +24,7 @@ import { plugins } from './plugins' interface ServerDef { apollo: ApolloServer app: Express - con: Connection + con: DbConnection } export const createServer = async ( @@ -37,7 +37,7 @@ export const createServer = async ( logger.debug('createServer...') // open mysql connection - const con = await connection() + const con = await Connection.getInstance() if (!con?.isConnected) { logger.fatal(`Couldn't open connection to database!`) throw new Error(`Fatal: Couldn't open connection to database`) diff --git a/backend/src/typeorm/connection.ts b/backend/src/typeorm/connection.ts index 7dec820b5..3c8307478 100644 --- a/backend/src/typeorm/connection.ts +++ b/backend/src/typeorm/connection.ts @@ -1,33 +1,55 @@ // TODO This is super weird - since the entities are defined in another project they have their own globals. // We cannot use our connection here, but must use the external typeorm installation -import { Connection, createConnection, FileLogger } from '@dbTools/typeorm' +import { Connection as DbConnection, createConnection, FileLogger } from '@dbTools/typeorm' import { entities } from '@entity/index' import { CONFIG } from '@/config' -export const connection = async (): Promise => { - try { - return createConnection({ - name: 'default', - type: 'mysql', - host: CONFIG.DB_HOST, - port: CONFIG.DB_PORT, - username: CONFIG.DB_USER, - password: CONFIG.DB_PASSWORD, - database: CONFIG.DB_DATABASE, - entities, - synchronize: false, - logging: true, - logger: new FileLogger('all', { - logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH, - }), - extra: { - charset: 'utf8mb4_unicode_ci', - }, - }) - } catch (error) { - // eslint-disable-next-line no-console - console.log(error) - return null +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +export class Connection { + private static instance: DbConnection + + /** + * The Singleton's constructor should always be private to prevent direct + * construction calls with the `new` operator. + */ + // eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function + private constructor() {} + + /** + * The static method that controls the access to the singleton instance. + * + * This implementation let you subclass the Singleton class while keeping + * just one instance of each subclass around. + */ + public static async getInstance(): Promise { + if (Connection.instance) { + return Connection.instance + } + try { + Connection.instance = await createConnection({ + name: 'default', + type: 'mysql', + host: CONFIG.DB_HOST, + port: CONFIG.DB_PORT, + username: CONFIG.DB_USER, + password: CONFIG.DB_PASSWORD, + database: CONFIG.DB_DATABASE, + entities, + synchronize: false, + logging: true, + logger: new FileLogger('all', { + logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH, + }), + extra: { + charset: 'utf8mb4_unicode_ci', + }, + }) + return Connection.instance + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + return null + } } } diff --git a/backend/src/util/executeKlicktipp.ts b/backend/src/util/executeKlicktipp.ts new file mode 100644 index 000000000..74b453307 --- /dev/null +++ b/backend/src/util/executeKlicktipp.ts @@ -0,0 +1,16 @@ +import { Connection } from '@/typeorm/connection' + +import { exportEventDataToKlickTipp } from './klicktipp' + +async function executeKlicktipp(): Promise { + const connection = await Connection.getInstance() + if (connection) { + await exportEventDataToKlickTipp() + await connection.close() + return true + } else { + return false + } +} + +void executeKlicktipp() diff --git a/backend/src/util/klicktipp.test.ts b/backend/src/util/klicktipp.test.ts new file mode 100644 index 000000000..6639a0aa4 --- /dev/null +++ b/backend/src/util/klicktipp.test.ts @@ -0,0 +1,65 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { Connection } from '@dbTools/typeorm' +import { Event as DbEvent } from '@entity/Event' +import { ApolloServerTestClient } from 'apollo-server-testing' + +import { testEnvironment, cleanDB, resetToken } from '@test/helpers' + +import { addFieldsToSubscriber } from '@/apis/KlicktippController' +import { creations } from '@/seeds/creation' +import { creationFactory } from '@/seeds/factory/creation' +import { userFactory } from '@/seeds/factory/user' +import { login } from '@/seeds/graphql/mutations' +import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' +import { peterLustig } from '@/seeds/users/peter-lustig' + +import { exportEventDataToKlickTipp } from './klicktipp' + +jest.mock('@/apis/KlicktippController') + +let mutate: ApolloServerTestClient['mutate'], con: Connection +let testEnv: { + mutate: ApolloServerTestClient['mutate'] + query: ApolloServerTestClient['query'] + con: Connection +} + +beforeAll(async () => { + testEnv = await testEnvironment() + mutate = testEnv.mutate + con = testEnv.con + await DbEvent.clear() +}) + +afterAll(async () => { + await cleanDB() + await con.close() +}) + +describe('klicktipp', () => { + beforeAll(async () => { + await userFactory(testEnv, bibiBloxberg) + await userFactory(testEnv, peterLustig) + const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de') + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await creationFactory(testEnv, bibisCreation!) + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(() => { + resetToken() + }) + + describe('exportEventDataToKlickTipp', () => { + it('calls the KlicktippController', async () => { + await exportEventDataToKlickTipp() + expect(addFieldsToSubscriber).toBeCalled() + }) + }) +}) diff --git a/backend/src/util/klicktipp.ts b/backend/src/util/klicktipp.ts index a0ba3c0f7..c07b3128a 100644 --- a/backend/src/util/klicktipp.ts +++ b/backend/src/util/klicktipp.ts @@ -1,14 +1,11 @@ +// eslint-disable @typescript-eslint/no-explicit-any import { User } from '@entity/User' -import { getKlickTippUser } from '@/apis/KlicktippController' -import { LogError } from '@/server/LogError' -import { connection } from '@/typeorm/connection' +import { getKlickTippUser, addFieldsToSubscriber } from '@/apis/KlicktippController' +import { EventType } from '@/event/EventType' +import { lastDateTimeEvents } from '@/graphql/resolver/util/eventList' export async function retrieveNotRegisteredEmails(): Promise { - const con = await connection() - if (!con) { - throw new LogError('No connection to database') - } const users = await User.find({ relations: ['emailContact'] }) const notRegisteredUser = [] for (const user of users) { @@ -20,10 +17,39 @@ export async function retrieveNotRegisteredEmails(): Promise { console.log(`${user.emailContact.email}`) } } - await con.close() // eslint-disable-next-line no-console console.log('User die nicht bei KlickTipp vorhanden sind: ', notRegisteredUser) return notRegisteredUser } -void retrieveNotRegisteredEmails() +async function klickTippSendFieldToUser( + events: { email: string; value: Date }[], + field: string, +): Promise { + for (const event of events) { + const time = event.value.setSeconds(0) + await addFieldsToSubscriber(event.email, { [field]: Math.trunc(time / 1000) }) + } +} + +export async function exportEventDataToKlickTipp(): Promise { + const lastLoginEvents = await lastDateTimeEvents(EventType.USER_LOGIN) + await klickTippSendFieldToUser(lastLoginEvents, 'field186060') + + const registeredEvents = await lastDateTimeEvents(EventType.USER_ACTIVATE_ACCOUNT) + await klickTippSendFieldToUser(registeredEvents, 'field186061') + + const receiveTransactionEvents = await lastDateTimeEvents(EventType.TRANSACTION_RECEIVE) + await klickTippSendFieldToUser(receiveTransactionEvents, 'field185674') + + const contributionCreateEvents = await lastDateTimeEvents(EventType.TRANSACTION_SEND) + await klickTippSendFieldToUser(contributionCreateEvents, 'field185673') + + const linkRedeemedEvents = await lastDateTimeEvents(EventType.TRANSACTION_LINK_REDEEM) + await klickTippSendFieldToUser(linkRedeemedEvents, 'field185676') + + const confirmContributionEvents = await lastDateTimeEvents(EventType.ADMIN_CONTRIBUTION_CONFIRM) + await klickTippSendFieldToUser(confirmContributionEvents, 'field185675') + + return true +} diff --git a/database/migrations/0066-x-community-sendcoins-transactions_table.ts b/database/migrations/0066-x-community-sendcoins-transactions_table.ts index 2a90f297a..425248c22 100644 --- a/database/migrations/0066-x-community-sendcoins-transactions_table.ts +++ b/database/migrations/0066-x-community-sendcoins-transactions_table.ts @@ -66,8 +66,6 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis ) } -/* eslint-disable @typescript-eslint/no-empty-function */ -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { await queryFn('ALTER TABLE `transactions` DROP COLUMN `user_gradido_id`;') await queryFn('ALTER TABLE `transactions` DROP COLUMN `user_name`;') diff --git a/database/package.json b/database/package.json index 23e358364..72d35dc04 100644 --- a/database/package.json +++ b/database/package.json @@ -1,6 +1,6 @@ { "name": "gradido-database", - "version": "1.20.0", + "version": "1.21.0", "description": "Gradido Database Tool to execute database migrations", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/database", diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index f20a8c2d1..2c237d22c 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -57,7 +57,7 @@ EMAIL_CODE_REQUEST_TIME=10 WEBHOOK_ELOPAGE_SECRET=secret # Federation -FEDERATION_DHT_CONFIG_VERSION=v2.2023-02-07 +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 diff --git a/dht-node/.env.template b/dht-node/.env.template index efe6158a6..a7603c15a 100644 --- a/dht-node/.env.template +++ b/dht-node/.env.template @@ -8,6 +8,10 @@ DB_PASSWORD=$DB_PASSWORD DB_DATABASE=gradido_community TYPEORM_LOGGING_RELATIVE_PATH=$TYPEORM_LOGGING_RELATIVE_PATH +# Community +COMMUNITY_NAME=$COMMUNITY_NAME +COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION + # Federation FEDERATION_DHT_CONFIG_VERSION=$FEDERATION_DHT_CONFIG_VERSION # if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen diff --git a/dht-node/jest.config.js b/dht-node/jest.config.js index fa00ed868..0b83d8edd 100644 --- a/dht-node/jest.config.js +++ b/dht-node/jest.config.js @@ -6,7 +6,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], coverageThreshold: { global: { - lines: 80, + lines: 83, }, }, setupFiles: ['/test/testSetup.ts'], diff --git a/dht-node/package.json b/dht-node/package.json index 1f8ba4505..d5dae0230 100644 --- a/dht-node/package.json +++ b/dht-node/package.json @@ -1,6 +1,6 @@ { "name": "gradido-dht-node", - "version": "1.20.0", + "version": "1.21.0", "description": "Gradido dht-node module", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/", @@ -23,7 +23,8 @@ "nodemon": "^2.0.20", "ts-node": "^10.9.1", "tsconfig-paths": "^4.1.2", - "typescript": "^4.9.4" + "typescript": "^4.9.4", + "uuid": "^8.3.2" }, "devDependencies": { "@types/dotenv": "^8.2.0", @@ -31,6 +32,7 @@ "@types/node": "^18.11.18", "@typescript-eslint/eslint-plugin": "^5.48.0", "@typescript-eslint/parser": "^5.48.0", + "@types/uuid": "^8.3.4", "eslint": "^8.31.0", "eslint-config-prettier": "^8.3.0", "eslint-config-standard": "^17.0.0", diff --git a/dht-node/src/config/index.ts b/dht-node/src/config/index.ts index eca5dbbb5..43949201b 100644 --- a/dht-node/src/config/index.ts +++ b/dht-node/src/config/index.ts @@ -9,7 +9,7 @@ const constants = { LOG_LEVEL: process.env.LOG_LEVEL || 'info', CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v2.2023-02-07', + EXPECTED: 'v3.2023-04-26', CURRENT: '', }, } @@ -28,6 +28,12 @@ const database = { process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.dht-node.log', } +const community = { + COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung', + COMMUNITY_DESCRIPTION: + process.env.COMMUNITY_DESCRIPTION || 'Gradido-Community einer lokalen Entwicklungsumgebung.', +} + const federation = { FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || 'GRADIDO_HUB', FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null, @@ -51,6 +57,7 @@ const CONFIG = { ...constants, ...server, ...database, + ...community, ...federation, } diff --git a/dht-node/src/dht_node/index.test.ts b/dht-node/src/dht_node/index.test.ts index e76e6ac9f..ec172c4f8 100644 --- a/dht-node/src/dht_node/index.test.ts +++ b/dht-node/src/dht_node/index.test.ts @@ -5,8 +5,10 @@ import { startDHT } from './index' import DHT from '@hyperswarm/dht' import CONFIG from '@/config' import { logger } from '@test/testSetup' +import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { testEnvironment, cleanDB } from '@test/helpers' +import { validate as validateUUID, version as versionUUID } from 'uuid' CONFIG.FEDERATION_DHT_SEED = '64ebcb0e3ad547848fef4197c6e2332f' @@ -114,6 +116,9 @@ describe('federation', () => { const hashSpy = jest.spyOn(DHT, 'hash') const keyPairSpy = jest.spyOn(DHT, 'keyPair') beforeEach(async () => { + CONFIG.FEDERATION_COMMUNITY_URL = 'https://test.gradido.net' + CONFIG.COMMUNITY_NAME = 'Gradido Test Community' + CONFIG.COMMUNITY_DESCRIPTION = 'Community to test the federation' DHT.mockClear() jest.clearAllMocks() await cleanDB() @@ -132,6 +137,64 @@ describe('federation', () => { expect(DHT).toBeCalledWith({ keyPair: keyPairMock }) }) + it('stores the home community in community table ', async () => { + const result = await DbCommunity.find() + expect(result).toEqual([ + expect.objectContaining({ + id: expect.any(Number), + foreign: false, + url: 'https://test.gradido.net/api/', + publicKey: expect.any(Buffer), + communityUuid: expect.any(String), + authenticatedAt: null, + name: 'Gradido Test Community', + description: 'Community to test the federation', + creationDate: expect.any(Date), + createdAt: expect.any(Date), + updatedAt: null, + }), + ]) + expect(validateUUID(result[0].communityUuid ? result[0].communityUuid : '')).toEqual(true) + expect(versionUUID(result[0].communityUuid ? result[0].communityUuid : '')).toEqual(4) + }) + + it('creates 3 entries in table federated_communities', async () => { + const result = await DbFederatedCommunity.find({ order: { id: 'ASC' } }) + await expect(result).toHaveLength(3) + await expect(result).toEqual([ + expect.objectContaining({ + id: expect.any(Number), + foreign: false, + publicKey: expect.any(Buffer), + apiVersion: '1_0', + endPoint: 'https://test.gradido.net/api/', + lastAnnouncedAt: null, + createdAt: expect.any(Date), + updatedAt: null, + }), + expect.objectContaining({ + id: expect.any(Number), + foreign: false, + publicKey: expect.any(Buffer), + apiVersion: '1_1', + endPoint: 'https://test.gradido.net/api/', + lastAnnouncedAt: null, + createdAt: expect.any(Date), + updatedAt: null, + }), + expect.objectContaining({ + id: expect.any(Number), + foreign: false, + publicKey: expect.any(Buffer), + apiVersion: '2_0', + endPoint: 'https://test.gradido.net/api/', + lastAnnouncedAt: null, + createdAt: expect.any(Date), + updatedAt: null, + }), + ]) + }) + describe('DHT node', () => { it('creates a server', () => { expect(nodeCreateServerMock).toBeCalled() @@ -780,21 +843,21 @@ describe('federation', () => { socketEventMocks.open() }) - it.skip('calls socket write with own api versions', () => { + it('calls socket write with own api versions', () => { expect(socketWriteMock).toBeCalledWith( Buffer.from( JSON.stringify([ { api: '1_0', - url: 'http://localhost/api/', + url: 'https://test.gradido.net/api/', }, { api: '1_1', - url: 'http://localhost/api/', + url: 'https://test.gradido.net/api/', }, { api: '2_0', - url: 'http://localhost/api/', + url: 'https://test.gradido.net/api/', }, ]), ), @@ -804,5 +867,101 @@ describe('federation', () => { }) }) }) + + describe('restart DHT', () => { + let homeCommunity: DbCommunity + let federatedCommunities: DbFederatedCommunity[] + + describe('without changes', () => { + beforeEach(async () => { + DHT.mockClear() + jest.clearAllMocks() + homeCommunity = (await DbCommunity.find())[0] + federatedCommunities = await DbFederatedCommunity.find({ order: { id: 'ASC' } }) + await startDHT(TEST_TOPIC) + }) + + it('does not change home community in community table except updated at column ', async () => { + await expect(DbCommunity.find()).resolves.toEqual([ + { + ...homeCommunity, + updatedAt: expect.any(Date), + }, + ]) + }) + + it('rewrites the 3 entries in table federated_communities', async () => { + const result = await DbFederatedCommunity.find() + await expect(result).toHaveLength(3) + await expect(result).toEqual([ + { + ...federatedCommunities[0], + id: expect.any(Number), + createdAt: expect.any(Date), + }, + { + ...federatedCommunities[1], + id: expect.any(Number), + createdAt: expect.any(Date), + }, + { + ...federatedCommunities[2], + id: expect.any(Number), + createdAt: expect.any(Date), + }, + ]) + }) + }) + + describe('changeing URL, name and description', () => { + beforeEach(async () => { + CONFIG.FEDERATION_COMMUNITY_URL = 'https://test2.gradido.net' + CONFIG.COMMUNITY_NAME = 'Second Gradido Test Community' + CONFIG.COMMUNITY_DESCRIPTION = 'Another Community to test the federation' + DHT.mockClear() + jest.clearAllMocks() + homeCommunity = (await DbCommunity.find())[0] + federatedCommunities = await DbFederatedCommunity.find({ order: { id: 'ASC' } }) + await startDHT(TEST_TOPIC) + }) + + it('updates URL, name, description and updated at columns ', async () => { + await expect(DbCommunity.find()).resolves.toEqual([ + { + ...homeCommunity, + url: 'https://test2.gradido.net/api/', + name: 'Second Gradido Test Community', + description: 'Another Community to test the federation', + updatedAt: expect.any(Date), + }, + ]) + }) + + it('rewrites the 3 entries in table federated_communities with new endpoint', async () => { + const result = await DbFederatedCommunity.find() + await expect(result).toHaveLength(3) + await expect(result).toEqual([ + { + ...federatedCommunities[0], + id: expect.any(Number), + createdAt: expect.any(Date), + endPoint: 'https://test2.gradido.net/api/', + }, + { + ...federatedCommunities[1], + id: expect.any(Number), + createdAt: expect.any(Date), + endPoint: 'https://test2.gradido.net/api/', + }, + { + ...federatedCommunities[2], + id: expect.any(Number), + createdAt: expect.any(Date), + endPoint: 'https://test2.gradido.net/api/', + }, + ]) + }) + }) + }) }) }) diff --git a/dht-node/src/dht_node/index.ts b/dht-node/src/dht_node/index.ts index 0db7a28c2..efd2ae2f8 100644 --- a/dht-node/src/dht_node/index.ts +++ b/dht-node/src/dht_node/index.ts @@ -4,10 +4,15 @@ import DHT from '@hyperswarm/dht' import { logger } from '@/server/logger' import CONFIG from '@/config' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' +import { Community as DbCommunity } from '@entity/Community' +import { v4 as uuidv4 } from 'uuid' const KEY_SECRET_SEEDBYTES = 32 -const getSeed = (): Buffer | null => - CONFIG.FEDERATION_DHT_SEED ? Buffer.alloc(KEY_SECRET_SEEDBYTES, CONFIG.FEDERATION_DHT_SEED) : null +const getSeed = (): Buffer | null => { + return CONFIG.FEDERATION_DHT_SEED + ? Buffer.alloc(KEY_SECRET_SEEDBYTES, CONFIG.FEDERATION_DHT_SEED) + : null +} const POLLTIME = 20000 const SUCCESSTIME = 120000 @@ -28,10 +33,12 @@ export const startDHT = async (topic: string): Promise => { try { const TOPIC = DHT.hash(Buffer.from(topic)) const keyPair = DHT.keyPair(getSeed()) - logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`) + const pubKeyString = keyPair.publicKey.toString('hex') + logger.info(`keyPairDHT: publicKey=${pubKeyString}`) logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`) + await writeHomeCommunityEntry(pubKeyString) - const ownApiVersions = await writeFederatedHomeCommunityEnries(keyPair.publicKey) + const ownApiVersions = await writeFederatedHomeCommunityEntries(pubKeyString) logger.info(`ApiList: ${JSON.stringify(ownApiVersions)}`) const node = new DHT({ keyPair }) @@ -138,7 +145,7 @@ export const startDHT = async (topic: string): Promise => { data.peers.forEach((peer: any) => { const pubKey = peer.publicKey.toString('hex') if ( - pubKey !== keyPair.publicKey.toString('hex') && + pubKey !== pubKeyString && !successfulRequests.includes(pubKey) && !errorfulRequests.includes(pubKey) && !collectedPubKeys.includes(pubKey) @@ -179,7 +186,7 @@ export const startDHT = async (topic: string): Promise => { } } -async function writeFederatedHomeCommunityEnries(pubKey: any): Promise { +async function writeFederatedHomeCommunityEntries(pubKey: string): Promise { const homeApiVersions: CommunityApi[] = Object.values(ApiVersionType).map(function (apiEnum) { const comApi: CommunityApi = { api: apiEnum, @@ -189,21 +196,65 @@ async function writeFederatedHomeCommunityEnries(pubKey: any): Promise { + try { + // check for existing homeCommunity entry + let homeCom = await DbCommunity.findOne({ + foreign: false, + publicKey: Buffer.from(pubKey), + }) + if (!homeCom) { + // check if a homecommunity with a different publicKey still exists + homeCom = await DbCommunity.findOne({ foreign: false }) + } + if (homeCom) { + // simply update the existing entry, but it MUST keep the ID and UUID because of possible relations + homeCom.publicKey = Buffer.from(pubKey) + homeCom.url = CONFIG.FEDERATION_COMMUNITY_URL + '/api/' + homeCom.name = CONFIG.COMMUNITY_NAME + homeCom.description = CONFIG.COMMUNITY_DESCRIPTION + await DbCommunity.save(homeCom) + logger.info(`home-community updated successfully:`, homeCom) + } else { + // insert a new homecommunity entry including a new ID and a new but ensured unique UUID + homeCom = new DbCommunity() + homeCom.foreign = false + homeCom.publicKey = Buffer.from(pubKey) + homeCom.communityUuid = await newCommunityUuid() + homeCom.url = CONFIG.FEDERATION_COMMUNITY_URL + '/api/' + homeCom.name = CONFIG.COMMUNITY_NAME + homeCom.description = CONFIG.COMMUNITY_DESCRIPTION + homeCom.creationDate = new Date() + await DbCommunity.insert(homeCom) + logger.info(`home-community inserted successfully:`, homeCom) + } + } catch (err) { + throw new Error(`Federation: Error writing HomeCommunity-Entry: ${err}`) + } +} + +const newCommunityUuid = async (): Promise => { + while (true) { + const communityUuid = uuidv4() + if ((await DbCommunity.count({ where: { communityUuid } })) === 0) { + return communityUuid + } + logger.info('CommunityUuid creation conflict...', communityUuid) + } +} diff --git a/dht-node/src/index.ts b/dht-node/src/index.ts index 2315c77df..d5e5f700b 100644 --- a/dht-node/src/index.ts +++ b/dht-node/src/index.ts @@ -21,9 +21,8 @@ async function main() { logger.fatal('Fatal: Database Version incorrect') throw new Error('Fatal: Database Version incorrect') } - - // eslint-disable-next-line no-console - console.log( + logger.debug(`dhtseed set by CONFIG.FEDERATION_DHT_SEED=${CONFIG.FEDERATION_DHT_SEED}`) + logger.info( `starting Federation on ${CONFIG.FEDERATION_DHT_TOPIC} ${ CONFIG.FEDERATION_DHT_SEED ? 'with seed...' : 'without seed...' }`, diff --git a/dht-node/test/helpers.ts b/dht-node/test/helpers.ts index aa7f94964..c5d6ce82b 100644 --- a/dht-node/test/helpers.ts +++ b/dht-node/test/helpers.ts @@ -22,8 +22,8 @@ const context = { export const cleanDB = async () => { // this only works as long we do not have foreign key constraints - for (let i = 0; i < entities.length; i++) { - await resetEntity(entities[i]) + for (const entity of entities) { + await resetEntity(entity) } } diff --git a/dht-node/yarn.lock b/dht-node/yarn.lock index 5832ecd8b..85c4d35fa 100644 --- a/dht-node/yarn.lock +++ b/dht-node/yarn.lock @@ -769,6 +769,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/uuid@^8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" @@ -4138,6 +4143,11 @@ url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" diff --git a/federation/package.json b/federation/package.json index 3173e77ab..861d3a077 100644 --- a/federation/package.json +++ b/federation/package.json @@ -1,6 +1,6 @@ { "name": "gradido-federation", - "version": "1.20.0", + "version": "1.21.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", diff --git a/frontend/package.json b/frontend/package.json index 7d89fba7c..c084a74e6 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "bootstrap-vue-gradido-wallet", - "version": "1.20.0", + "version": "1.21.0", "private": true, "scripts": { "start": "node run/server.js", @@ -50,6 +50,7 @@ "prettier": "^2.2.1", "qrcanvas-vue": "2.1.1", "regenerator-runtime": "^0.13.7", + "uuid": "^9.0.0", "vee-validate": "^3.4.5", "vue": "2.6.12", "vue-apollo": "^3.0.7", diff --git a/frontend/src/components/GddSend/TransactionForm.spec.js b/frontend/src/components/GddSend/TransactionForm.spec.js index 41f69960e..e4cee20be 100644 --- a/frontend/src/components/GddSend/TransactionForm.spec.js +++ b/frontend/src/components/GddSend/TransactionForm.spec.js @@ -71,9 +71,9 @@ describe('TransactionForm', () => { }) describe('with balance <= 0.00 GDD the form is disabled', () => { - it('has a disabled input field of type email', () => { + it('has a disabled input field of type text', () => { expect( - wrapper.find('div[data-test="input-email"]').find('input').attributes('disabled'), + wrapper.find('div[data-test="input-identifier"]').find('input').attributes('disabled'), ).toBe('disabled') }) @@ -116,51 +116,54 @@ describe('TransactionForm', () => { expect(wrapper.vm.radioSelected).toBe(SEND_TYPES.send) }) - describe('email field', () => { - it('has an input field of type email', () => { + describe('identifier field', () => { + it('has an input field of type text', () => { expect( - wrapper.find('div[data-test="input-email"]').find('input').attributes('type'), - ).toBe('email') + wrapper.find('div[data-test="input-identifier"]').find('input').attributes('type'), + ).toBe('text') }) - it('has a label form.receiver', () => { - expect(wrapper.find('div[data-test="input-email"]').find('label').text()).toBe( + it('has a label form.recipient', () => { + expect(wrapper.find('div[data-test="input-identifier"]').find('label').text()).toBe( 'form.recipient', ) }) - it('has a placeholder "E-Mail"', () => { + it('has a placeholder for identifier', () => { expect( - wrapper.find('div[data-test="input-email"]').find('input').attributes('placeholder'), - ).toBe('form.email') + wrapper + .find('div[data-test="input-identifier"]') + .find('input') + .attributes('placeholder'), + ).toBe('form.identifier') }) - it('flushes an error message when no valid email is given', async () => { - await wrapper.find('div[data-test="input-email"]').find('input').setValue('a') + it('flushes an error message when no valid identifier is given', async () => { + await wrapper.find('div[data-test="input-identifier"]').find('input').setValue('a') await flushPromises() expect( - wrapper.find('div[data-test="input-email"]').find('.invalid-feedback').text(), - ).toBe('validations.messages.email') + wrapper.find('div[data-test="input-identifier"]').find('.invalid-feedback').text(), + ).toBe('form.validation.valid-identifier') }) // TODO:SKIPPED there is no check that the email being sent to is the same as the user's email. it.skip('flushes an error message when email is the email of logged in user', async () => { await wrapper - .find('div[data-test="input-email"]') + .find('div[data-test="input-identifier"]') .find('input') .setValue('user@example.org') await flushPromises() expect( - wrapper.find('div[data-test="input-email"]').find('.invalid-feedback').text(), + wrapper.find('div[data-test="input-identifier"]').find('.invalid-feedback').text(), ).toBe('form.validation.is-not') }) - it('trims the email after blur', async () => { + it('trims the identifier after blur', async () => { await wrapper - .find('div[data-test="input-email"]') + .find('div[data-test="input-identifier"]') .find('input') .setValue(' valid@email.com ') - await wrapper.find('div[data-test="input-email"]').find('input').trigger('blur') + await wrapper.find('div[data-test="input-identifier"]').find('input').trigger('blur') await flushPromises() expect(wrapper.vm.form.identifier).toBe('valid@email.com') }) @@ -304,7 +307,7 @@ Die ganze Welt bezwingen.“`) it('clears all fields on click', async () => { await wrapper - .find('div[data-test="input-email"]') + .find('div[data-test="input-identifier"]') .find('input') .setValue('someone@watches.tv') await wrapper.find('div[data-test="input-amount"]').find('input').setValue('87.23') @@ -327,7 +330,7 @@ Die ganze Welt bezwingen.“`) describe('submit', () => { beforeEach(async () => { await wrapper - .find('div[data-test="input-email"]') + .find('div[data-test="input-identifier"]') .find('input') .setValue('someone@watches.tv') await wrapper.find('div[data-test="input-amount"]').find('input').setValue('87.23') @@ -380,8 +383,8 @@ Die ganze Welt bezwingen.“`) }) describe('query for username with success', () => { - it('has no email input field', () => { - expect(wrapper.find('div[data-test="input-email"]').exists()).toBe(false) + it('has no identifier input field', () => { + expect(wrapper.find('div[data-test="input-identifier"]').exists()).toBe(false) }) it('queries the username', () => { diff --git a/frontend/src/components/GddSend/TransactionForm.vue b/frontend/src/components/GddSend/TransactionForm.vue index ab56703b2..d5b67d547 100644 --- a/frontend/src/components/GddSend/TransactionForm.vue +++ b/frontend/src/components/GddSend/TransactionForm.vue @@ -59,10 +59,10 @@
- diff --git a/frontend/src/components/LinkInformations/RedeemInformation.vue b/frontend/src/components/LinkInformations/RedeemInformation.vue index d287605a4..7a55c5c01 100644 --- a/frontend/src/components/LinkInformations/RedeemInformation.vue +++ b/frontend/src/components/LinkInformations/RedeemInformation.vue @@ -1,11 +1,12 @@ @@ -47,12 +48,13 @@ export default { return { linkData: { __typename: 'TransactionLink', - amount: '123.45', - memo: 'memo', + amount: '', + memo: '', user: { - firstName: 'Bibi', + firstName: '', }, deletedAt: null, + validLink: false, }, } }, @@ -67,13 +69,14 @@ export default { }, }) .then((result) => { + this.validLink = true this.linkData = result.data.queryTransactionLink if (this.linkData.__typename === 'ContributionLink' && this.$store.state.token) { this.mutationLink(this.linkData.amount) } }) - .catch((err) => { - this.toastError(err.message) + .catch(() => { + this.toastError(this.$t('gdd_per_link.redeemlink-error')) }) }, mutationLink(amount) { diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js index 7716d00de..4036626d8 100644 --- a/frontend/src/store/store.js +++ b/frontend/src/store/store.js @@ -53,6 +53,9 @@ export const mutations = { hideAmountGDT: (state, hideAmountGDT) => { state.hideAmountGDT = !!hideAmountGDT }, + email: (state, email) => { + state.email = email || '' + }, } export const actions = { @@ -81,6 +84,7 @@ export const actions = { commit('isAdmin', false) commit('hideAmountGDD', false) commit('hideAmountGDT', true) + commit('email', '') localStorage.clear() }, } @@ -109,6 +113,7 @@ try { publisherId: null, hideAmountGDD: null, hideAmountGDT: null, + email: '', }, getters: {}, // Syncronous mutation of the state diff --git a/frontend/src/store/store.test.js b/frontend/src/store/store.test.js index 116594e77..a6a596209 100644 --- a/frontend/src/store/store.test.js +++ b/frontend/src/store/store.test.js @@ -33,6 +33,7 @@ const { hasElopage, hideAmountGDD, hideAmountGDT, + email, } = mutations const { login, logout } = actions @@ -166,6 +167,14 @@ describe('Vuex store', () => { expect(state.hideAmountGDT).toEqual(true) }) }) + + describe('email', () => { + it('sets the state of email', () => { + const state = { email: '' } + email(state, 'peter@luatig.de') + expect(state.email).toEqual('peter@luatig.de') + }) + }) }) describe('actions', () => { @@ -253,9 +262,9 @@ describe('Vuex store', () => { const commit = jest.fn() const state = {} - it('calls eleven commits', () => { + it('calls twelve commits', () => { logout({ commit, state }) - expect(commit).toHaveBeenCalledTimes(11) + expect(commit).toHaveBeenCalledTimes(12) }) it('commits token', () => { @@ -312,6 +321,12 @@ describe('Vuex store', () => { logout({ commit, state }) expect(commit).toHaveBeenNthCalledWith(11, 'hideAmountGDT', true) }) + + it('commits email', () => { + logout({ commit, state }) + expect(commit).toHaveBeenNthCalledWith(12, 'email', '') + }) + // how to get this working? it.skip('calls localStorage.clear()', () => { const clearStorageMock = jest.fn() diff --git a/frontend/src/validation-rules.js b/frontend/src/validation-rules.js index 124ef8528..adb4dc431 100644 --- a/frontend/src/validation-rules.js +++ b/frontend/src/validation-rules.js @@ -2,6 +2,13 @@ import { configure, extend } from 'vee-validate' // eslint-disable-next-line camelcase import { required, email, min, max, is_not } from 'vee-validate/dist/rules' import { checkUsername } from '@/graphql/queries' +import { validate as validateUuid, version as versionUuid } from 'uuid' + +// taken from vee-validate +// eslint-disable-next-line no-useless-escape +const EMAIL_REGEX = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + +const USERNAME_REGEX = /^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/ export const loadAllRules = (i18nCallback, apollo) => { configure({ @@ -141,7 +148,7 @@ export const loadAllRules = (i18nCallback, apollo) => { extend('usernameUnique', { validate(value) { - if (!value.match(/^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/)) return true + if (!value.match(USERNAME_REGEX)) return true return apollo .query({ query: checkUsername, @@ -155,4 +162,14 @@ export const loadAllRules = (i18nCallback, apollo) => { }, message: (_, values) => i18nCallback.t('form.validation.username-unique', values), }) + + extend('validIdentifier', { + validate(value) { + const isEmail = !!EMAIL_REGEX.test(value) + const isUsername = !!value.match(USERNAME_REGEX) + const isGradidoId = validateUuid(value) && versionUuid(value) === 4 + return isEmail || isUsername || isGradidoId + }, + message: (_, values) => i18nCallback.t('form.validation.valid-identifier', values), + }) } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 7cc8e5fe5..8eff12aaf 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -14176,6 +14176,11 @@ uuid@^8.3.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" diff --git a/package.json b/package.json index 7845d722b..04c8d46c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gradido", - "version": "1.20.0", + "version": "1.21.0", "description": "Gradido", "main": "index.js", "repository": "git@github.com:gradido/gradido.git",