From 55ee3a0b6bda7298c10f1c40cf23c8e5d419c013 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 21 Feb 2023 17:14:09 +0100 Subject: [PATCH 001/153] feat(backend): alias in update user info --- .../src/graphql/arg/UpdateUserInfosArgs.ts | 5 + .../src/graphql/resolver/UserResolver.test.ts | 123 ++++++++++++++++++ backend/src/graphql/resolver/UserResolver.ts | 18 +++ backend/src/seeds/graphql/mutations.ts | 2 + 4 files changed, 148 insertions(+) diff --git a/backend/src/graphql/arg/UpdateUserInfosArgs.ts b/backend/src/graphql/arg/UpdateUserInfosArgs.ts index b45539487..cde5f7732 100644 --- a/backend/src/graphql/arg/UpdateUserInfosArgs.ts +++ b/backend/src/graphql/arg/UpdateUserInfosArgs.ts @@ -1,4 +1,5 @@ import { ArgsType, Field } from 'type-graphql' +// import { Length } from 'class-validator' @ArgsType() export default class UpdateUserInfosArgs { @@ -8,6 +9,10 @@ export default class UpdateUserInfosArgs { @Field({ nullable: true }) lastName?: string + @Field({ nullable: true }) + // @Length(5, 20) + alias?: string + @Field({ nullable: true }) language?: string diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 19eb04b34..89042ebdc 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -1134,6 +1134,129 @@ describe('UserResolver', () => { }) }) + describe('alias', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('too short', () => { + it('throws and logs an error', async () => { + await expect( + mutate({ + mutation: updateUserInfos, + variables: { + alias: 'bibi', + }, + }), + ).resolves.toMatchObject({ + errors: [new GraphQLError('Given alias is too short')], + data: null, + }) + expect(logger.error).toBeCalledWith('Given alias is too short', 'bibi') + }) + }) + + describe('too long', () => { + it('throws and logs an error', async () => { + await expect( + mutate({ + mutation: updateUserInfos, + variables: { + alias: 'bibis_alias_far_too_long', + }, + }), + ).resolves.toMatchObject({ + errors: [new GraphQLError('Given alias is too long')], + data: null, + }) + expect(logger.error).toBeCalledWith( + 'Given alias is too long', + 'bibis_alias_far_too_long', + ) + }) + }) + + describe('invalid characters', () => { + it('throws and logs an error', async () => { + await expect( + mutate({ + mutation: updateUserInfos, + variables: { + alias: 'no_underscore', + }, + }), + ).resolves.toMatchObject({ + errors: [new GraphQLError('Invalid characters in alias')], + data: null, + }) + expect(logger.error).toBeCalledWith('Invalid characters in alias', 'no_underscore') + }) + }) + + describe('alias exists', () => { + let peter: User + beforeAll(async () => { + peter = await userFactory(testEnv, peterLustig) + await mutate({ + mutation: login, + variables: { + email: 'peter@lustig.de', + password: 'Aa12345_', + }, + }) + await mutate({ + mutation: updateUserInfos, + variables: { + alias: 'bibiBloxberg', + }, + }) + await mutate({ + mutation: login, + variables: { + email: 'bibi@bloxberg.de', + password: 'Aa12345_', + }, + }) + }) + + afterAll(async () => { + const [user] = await User.find({ id: peter.id }) + await user.remove() + }) + + it('throws and logs an error', async () => { + await expect( + mutate({ + mutation: updateUserInfos, + variables: { + alias: 'bibiBloxberg', + }, + }), + ).resolves.toMatchObject({ + errors: [new GraphQLError('Alias already in use')], + data: null, + }) + expect(logger.error).toBeCalledWith('Alias already in use', 'bibiBloxberg') + }) + }) + + describe('valid alias', () => { + it('updates the user in DB', async () => { + await mutate({ + mutation: updateUserInfos, + variables: { + alias: 'bibiBloxberg', + }, + }) + await expect(User.findOne()).resolves.toEqual( + expect.objectContaining({ + alias: 'bibiBloxberg', + }), + ) + }) + }) + }) + describe('language is not valid', () => { it('throws an error', async () => { jest.clearAllMocks() diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index f9617b0df..0c7ca79cb 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -517,6 +517,7 @@ export class UserResolver { { firstName, lastName, + alias, language, password, passwordNew, @@ -536,6 +537,23 @@ export class UserResolver { userEntity.lastName = lastName } + if (alias) { + if (alias.length < 5) { + throw new LogError('Given alias is too short', alias) + } + if (alias.length > 20) { + throw new LogError('Given alias is too long', alias) + } + if (!alias.match(/^[0-9A-Za-z]+$/)) { + throw new LogError('Invalid characters in alias', alias) + } + const aliasInUse = await DbUser.find({ alias }) + if (aliasInUse.length !== 0) { + throw new LogError('Alias already in use', alias) + } + userEntity.alias = alias + } + if (language) { if (!isLanguage(language)) { throw new LogError('Given language is not a valid language', language) diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index 4f9cbdeff..e0141b752 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -28,6 +28,7 @@ export const updateUserInfos = gql` mutation ( $firstName: String $lastName: String + $alias: String $password: String $passwordNew: String $locale: String @@ -37,6 +38,7 @@ export const updateUserInfos = gql` updateUserInfos( firstName: $firstName lastName: $lastName + alias: $alias password: $password passwordNew: $passwordNew language: $locale From 22f77459aab73fd34f17793a4e0ecaf3da1e84a4 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 24 Feb 2023 14:03:12 +0100 Subject: [PATCH 002/153] remove class validator comments --- backend/src/graphql/arg/UpdateUserInfosArgs.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/src/graphql/arg/UpdateUserInfosArgs.ts b/backend/src/graphql/arg/UpdateUserInfosArgs.ts index cde5f7732..dbf68c43c 100644 --- a/backend/src/graphql/arg/UpdateUserInfosArgs.ts +++ b/backend/src/graphql/arg/UpdateUserInfosArgs.ts @@ -1,5 +1,4 @@ import { ArgsType, Field } from 'type-graphql' -// import { Length } from 'class-validator' @ArgsType() export default class UpdateUserInfosArgs { @@ -10,7 +9,6 @@ export default class UpdateUserInfosArgs { lastName?: string @Field({ nullable: true }) - // @Length(5, 20) alias?: string @Field({ nullable: true }) From 3afc7b08fa7264246862110c31a1b72de7c8b7db Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 24 Feb 2023 14:15:52 +0100 Subject: [PATCH 003/153] update regex --- backend/src/graphql/resolver/UserResolver.test.ts | 8 ++++---- backend/src/graphql/resolver/UserResolver.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 89042ebdc..bbfbd1ddc 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -1182,14 +1182,14 @@ describe('UserResolver', () => { mutate({ mutation: updateUserInfos, variables: { - alias: 'no_underscore', + alias: 'no+äöllll', }, }), ).resolves.toMatchObject({ errors: [new GraphQLError('Invalid characters in alias')], data: null, }) - expect(logger.error).toBeCalledWith('Invalid characters in alias', 'no_underscore') + expect(logger.error).toBeCalledWith('Invalid characters in alias', 'no+äöllll') }) }) @@ -1245,12 +1245,12 @@ describe('UserResolver', () => { await mutate({ mutation: updateUserInfos, variables: { - alias: 'bibiBloxberg', + alias: 'bibi_Bloxberg', }, }) await expect(User.findOne()).resolves.toEqual( expect.objectContaining({ - alias: 'bibiBloxberg', + alias: 'bibi_Bloxberg', }), ) }) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 0c7ca79cb..2a13ee037 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -544,7 +544,7 @@ export class UserResolver { if (alias.length > 20) { throw new LogError('Given alias is too long', alias) } - if (!alias.match(/^[0-9A-Za-z]+$/)) { + if (!alias.match(/^[0-9A-Za-z]([_-]?[A-Za-z0-9])+$/)) { throw new LogError('Invalid characters in alias', alias) } const aliasInUse = await DbUser.find({ alias }) From 19a980b47bb9cf219e2fca81bab6ad6e86465ac6 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 24 Feb 2023 14:16:41 +0100 Subject: [PATCH 004/153] Update backend/src/graphql/resolver/UserResolver.test.ts Co-authored-by: Ulf Gebhardt --- backend/src/graphql/resolver/UserResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index bbfbd1ddc..b6b24949b 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -1220,7 +1220,7 @@ describe('UserResolver', () => { }) afterAll(async () => { - const [user] = await User.find({ id: peter.id }) + const user = await User.findOne({ id: peter.id }) await user.remove() }) From b63513e68ad1b9e49284c794d95e40c79b6e2612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 24 Mar 2023 00:47:30 +0100 Subject: [PATCH 005/153] first draft of db-modell --- .../TechnicalRequirements/Federation.md | 4 ++++ ...ramm_communities-communities_federation.png | Bin 0 -> 115492 bytes 2 files changed, 4 insertions(+) create mode 100644 docu/Concepts/TechnicalRequirements/image/classdiagramm_communities-communities_federation.png diff --git a/docu/Concepts/TechnicalRequirements/Federation.md b/docu/Concepts/TechnicalRequirements/Federation.md index 8bbd74a3e..b559da115 100644 --- a/docu/Concepts/TechnicalRequirements/Federation.md +++ b/docu/Concepts/TechnicalRequirements/Federation.md @@ -50,6 +50,10 @@ Before starting in describing the details of the federation handshake, some prer With the federation additional data tables/entities have to be created. +The following diagramm shows the first draft of a possible database-model base on the migration 0063-event_link_fields.ts + +![img](./image/classdiagramm_communities-communities_federation.png) + ##### Community-Entity Create the new *Community* table to store attributes of the own community. This table is used more like a frame for own community data in the future like the list of federated foreign communities, own users, own futher accounts like AUF- and Welfare-account and the profile data of the own community: diff --git a/docu/Concepts/TechnicalRequirements/image/classdiagramm_communities-communities_federation.png b/docu/Concepts/TechnicalRequirements/image/classdiagramm_communities-communities_federation.png new file mode 100644 index 0000000000000000000000000000000000000000..a794f9e7529c94d10274dba5573dff661cde9218 GIT binary patch literal 115492 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 Date: Fri, 24 Mar 2023 08:52:27 +0100 Subject: [PATCH 006/153] feat(backend): add event for subscribe&unsubscribe --- backend/src/event/EventType.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/event/EventType.ts b/backend/src/event/EventType.ts index b219a49ba..7b1fc2dc9 100644 --- a/backend/src/event/EventType.ts +++ b/backend/src/event/EventType.ts @@ -18,8 +18,10 @@ export enum EventType { REDEEM_REGISTER = 'REDEEM_REGISTER', SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = 'SEND_ACCOUNT_MULTIREGISTRATION_EMAIL', SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL', + SUBSCRIBE_NEWSLETTER = 'SUBSCRIBE_NEWSLETTER', TRANSACTION_SEND = 'TRANSACTION_SEND', TRANSACTION_RECEIVE = 'TRANSACTION_RECEIVE', + UNSUBSCRIBE_NEWSLETTER = 'UNSUBSCRIBE_NEWSLETTER', // VISIT_GRADIDO = 'VISIT_GRADIDO', // VERIFY_REDEEM = 'VERIFY_REDEEM', // INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT', From 7e838389a73a7a2726c238dbfc9d1ef5c8dcf51f Mon Sep 17 00:00:00 2001 From: elweyn Date: Fri, 24 Mar 2023 08:56:33 +0100 Subject: [PATCH 007/153] Event for subscribe newsletter --- backend/src/event/EVENT_SUBSCRIBE_NEWSLETTER.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 backend/src/event/EVENT_SUBSCRIBE_NEWSLETTER.ts diff --git a/backend/src/event/EVENT_SUBSCRIBE_NEWSLETTER.ts b/backend/src/event/EVENT_SUBSCRIBE_NEWSLETTER.ts new file mode 100644 index 000000000..f4207d059 --- /dev/null +++ b/backend/src/event/EVENT_SUBSCRIBE_NEWSLETTER.ts @@ -0,0 +1,6 @@ +import { User as DbUser } from '@entity/User' +import { Event as DbEvent } from '@entity/Event' +import { Event, EventType } from './Event' + +export const EVENT_SUBSCRIBE_NEWSLETTER = async (user: DbUser): Promise => + Event(EventType.SUBSCRIBE_NEWSLETTER, user, user).save() From f8549ba208c3257fe7ecb19bd26d01ebc956f8f4 Mon Sep 17 00:00:00 2001 From: elweyn Date: Fri, 24 Mar 2023 08:56:53 +0100 Subject: [PATCH 008/153] Event for unsubscribe newsletter --- backend/src/event/EVENT_UNSUBSCRIBE_NEWSLETTER.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 backend/src/event/EVENT_UNSUBSCRIBE_NEWSLETTER.ts diff --git a/backend/src/event/EVENT_UNSUBSCRIBE_NEWSLETTER.ts b/backend/src/event/EVENT_UNSUBSCRIBE_NEWSLETTER.ts new file mode 100644 index 000000000..6a6946e3f --- /dev/null +++ b/backend/src/event/EVENT_UNSUBSCRIBE_NEWSLETTER.ts @@ -0,0 +1,6 @@ +import { User as DbUser } from '@entity/User' +import { Event as DbEvent } from '@entity/Event' +import { Event, EventType } from './Event' + +export const EVENT_UNSUBSCRIBE_NEWSLETTER = async (user: DbUser): Promise => + Event(EventType.UNSUBSCRIBE_NEWSLETTER, user, user).save() From 04e6d7abeb6e3f56b741e72dfcc405664aad49b4 Mon Sep 17 00:00:00 2001 From: elweyn Date: Fri, 24 Mar 2023 08:58:02 +0100 Subject: [PATCH 009/153] Add new defined events to Event --- backend/src/event/Event.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/event/Event.ts b/backend/src/event/Event.ts index 2e7cca6af..4d6f47674 100644 --- a/backend/src/event/Event.ts +++ b/backend/src/event/Event.ts @@ -53,5 +53,7 @@ export { EVENT_LOGIN } from './EVENT_LOGIN' export { EVENT_REGISTER } from './EVENT_REGISTER' export { EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL } from './EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL' export { EVENT_SEND_CONFIRMATION_EMAIL } from './EVENT_SEND_CONFIRMATION_EMAIL' +export { EVENT_SUBSCRIBE_NEWSLETTER } from './EVENT_SUBSCRIBE_NEWSLETTER' export { EVENT_TRANSACTION_SEND } from './EVENT_TRANSACTION_SEND' export { EVENT_TRANSACTION_RECEIVE } from './EVENT_TRANSACTION_RECEIVE' +export { EVENT_UNSUBSCRIBE_NEWSLETTER } from './EVENT_UNSUBSCRIBE_NEWSLETTER' From 1945570146609c4e41c77db8d0fbbadd97db2cb4 Mon Sep 17 00:00:00 2001 From: elweyn Date: Fri, 24 Mar 2023 08:58:32 +0100 Subject: [PATCH 010/153] Call new Events --- backend/src/graphql/resolver/KlicktippResolver.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/src/graphql/resolver/KlicktippResolver.ts b/backend/src/graphql/resolver/KlicktippResolver.ts index 661aeb4a6..10be59363 100644 --- a/backend/src/graphql/resolver/KlicktippResolver.ts +++ b/backend/src/graphql/resolver/KlicktippResolver.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-return */ import { Resolver, Query, Authorized, Arg, Mutation, Ctx } from 'type-graphql' @@ -7,6 +8,7 @@ import { unsubscribe, klicktippSignIn, } from '@/apis/KlicktippController' +import { EVENT_UNSUBSCRIBE_NEWSLETTER, EVENT_SUBSCRIBE_NEWSLETTER } from '@/event/Event' import { RIGHTS } from '@/auth/RIGHTS' import { Context, getUser } from '@/server/context' @@ -28,6 +30,7 @@ export class KlicktippResolver { @Mutation(() => Boolean) async unsubscribeNewsletter(@Ctx() context: Context): Promise { const user = getUser(context) + await EVENT_UNSUBSCRIBE_NEWSLETTER(user) return await unsubscribe(user.emailContact.email) } @@ -38,6 +41,7 @@ export class KlicktippResolver { @Ctx() context: Context, ): Promise { const user = getUser(context) + await EVENT_SUBSCRIBE_NEWSLETTER(user) return await klicktippSignIn(user.emailContact.email, language) } } From 79bede5e35cb8f01e67f2196b8f229aad9d0367c Mon Sep 17 00:00:00 2001 From: elweyn Date: Fri, 24 Mar 2023 09:33:52 +0100 Subject: [PATCH 011/153] feat(backend): add fields to subscriber --- backend/src/apis/KlicktippController.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/backend/src/apis/KlicktippController.ts b/backend/src/apis/KlicktippController.ts index a291bb945..bb3e79831 100644 --- a/backend/src/apis/KlicktippController.ts +++ b/backend/src/apis/KlicktippController.ts @@ -79,3 +79,16 @@ export const getKlicktippTagMap = async () => { } return '' } + +export const addFieldsToSubscriber = async ( + email: string, + fields: any = {}, + newemail = '', + newsmsnumber = '', +) => { + const isLogin = await loginKlicktippUser() + if (isLogin) { + const subscriberId = await klicktippConnector.subscriberSearch(email) + return await klicktippConnector.subscriberUpdate(subscriberId, fields, newemail, newsmsnumber) + } +} From 595503b9707a59ac6833f8aaa85b34b2428f9308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 24 Mar 2023 17:26:13 +0100 Subject: [PATCH 012/153] divide the database migration in three steps --- .../TechnicalRequirements/Federation.md | 19 ++++++++- ...classdiagramm_x-community-readyness.drawio | 40 +++++++++++++++++++ ...ssdiagramm_x-community-readyness_step1.svg | 1 + ...ssdiagramm_x-community-readyness_step2.svg | 1 + ...ssdiagramm_x-community-readyness_step3.svg | 1 + 5 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 docu/Concepts/TechnicalRequirements/graphics/classdiagramm_x-community-readyness.drawio create mode 100644 docu/Concepts/TechnicalRequirements/image/classdiagramm_x-community-readyness_step1.svg create mode 100644 docu/Concepts/TechnicalRequirements/image/classdiagramm_x-community-readyness_step2.svg create mode 100644 docu/Concepts/TechnicalRequirements/image/classdiagramm_x-community-readyness_step3.svg diff --git a/docu/Concepts/TechnicalRequirements/Federation.md b/docu/Concepts/TechnicalRequirements/Federation.md index b559da115..95b37c7a1 100644 --- a/docu/Concepts/TechnicalRequirements/Federation.md +++ b/docu/Concepts/TechnicalRequirements/Federation.md @@ -50,7 +50,24 @@ Before starting in describing the details of the federation handshake, some prer With the federation additional data tables/entities have to be created. -The following diagramm shows the first draft of a possible database-model base on the migration 0063-event_link_fields.ts +The following diagramms shows the first draft of the possible database-model base on the migration 0063-event_link_fields.ts with 3 steps of migration to reach the required entities. All three diagramms are not exhaustive and are still a base for discussions: + +![img](./image/classdiagramm_x-community-readyness_step1.svg) + +In the first step the current communities table will be renamed to communities_federation. A new table communities is created. Because of the dynamic in the communities_federation data during dht-federation the relation between both entities will be on the collumn communities.communities_federation_public_key. This relation will allow to read a community-entry including its relation to the multi federation entries per api-version with the public key as identifier. + + +![img](./image/classdiagramm_x-community-readyness_step2.svg) + +The 2nd step is an introduction of the entity accounts between the users and the transactions table. This will cause a separation of the transactions from the users, to avoid possible conflicts or dependencies between local users of the community and remote users of foreign users, who will be part of x-communitiy-transactions. + + +![img](./image/classdiagramm_x-community-readyness_step3.svg) + +The 3rd step will introduce an additional foreign-users and a users_favorites table. A foreign_user could be stored in the existing users-table, but he will not need all the attributes of a home-user, especially he will never gets an AGE-account in this community. The user_favorites entity is designed to buildup the relations between users and foreign_users or in general between all users. This is simply a first idea for a future discussion. + + + ![img](./image/classdiagramm_communities-communities_federation.png) diff --git a/docu/Concepts/TechnicalRequirements/graphics/classdiagramm_x-community-readyness.drawio b/docu/Concepts/TechnicalRequirements/graphics/classdiagramm_x-community-readyness.drawio new file mode 100644 index 000000000..bb6f896c0 --- /dev/null +++ b/docu/Concepts/TechnicalRequirements/graphics/classdiagramm_x-community-readyness.drawio @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docu/Concepts/TechnicalRequirements/image/classdiagramm_x-community-readyness_step1.svg b/docu/Concepts/TechnicalRequirements/image/classdiagramm_x-community-readyness_step1.svg new file mode 100644 index 000000000..28bf0d314 --- /dev/null +++ b/docu/Concepts/TechnicalRequirements/image/classdiagramm_x-community-readyness_step1.svg @@ -0,0 +1 @@ +
X-Community-Readyness
Step 1
X-Community-Read...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docu/Concepts/TechnicalRequirements/image/classdiagramm_x-community-readyness_step2.svg b/docu/Concepts/TechnicalRequirements/image/classdiagramm_x-community-readyness_step2.svg new file mode 100644 index 000000000..41300d046 --- /dev/null +++ b/docu/Concepts/TechnicalRequirements/image/classdiagramm_x-community-readyness_step2.svg @@ -0,0 +1 @@ +
X-Community-Readyness
Step 2
X-Community-Read...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docu/Concepts/TechnicalRequirements/image/classdiagramm_x-community-readyness_step3.svg b/docu/Concepts/TechnicalRequirements/image/classdiagramm_x-community-readyness_step3.svg new file mode 100644 index 000000000..382c9d728 --- /dev/null +++ b/docu/Concepts/TechnicalRequirements/image/classdiagramm_x-community-readyness_step3.svg @@ -0,0 +1 @@ +
X-Community-Readyness
Step 3
X-Community-Read...
Text is not SVG - cannot display
\ No newline at end of file From bf9831a03f4cb08bb1d7712a47cd36dc799a61c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 28 Mar 2023 23:11:53 +0200 Subject: [PATCH 013/153] second vision draft of db-migration --- .../TechnicalRequirements/Federation.md | 18 +++++++-- ...classdiagramm_x-community-readyness.drawio | 36 +++++++++--------- .../image/class-diagramm_vision-draft2.png | Bin 0 -> 79299 bytes .../image/class-diagramm_vision-draft2.svg | 1 + 4 files changed, 34 insertions(+), 21 deletions(-) create mode 100644 docu/Concepts/TechnicalRequirements/image/class-diagramm_vision-draft2.png create mode 100644 docu/Concepts/TechnicalRequirements/image/class-diagramm_vision-draft2.svg diff --git a/docu/Concepts/TechnicalRequirements/Federation.md b/docu/Concepts/TechnicalRequirements/Federation.md index 95b37c7a1..2ab4272f2 100644 --- a/docu/Concepts/TechnicalRequirements/Federation.md +++ b/docu/Concepts/TechnicalRequirements/Federation.md @@ -56,20 +56,32 @@ The following diagramms shows the first draft of the possible database-model bas In the first step the current communities table will be renamed to communities_federation. A new table communities is created. Because of the dynamic in the communities_federation data during dht-federation the relation between both entities will be on the collumn communities.communities_federation_public_key. This relation will allow to read a community-entry including its relation to the multi federation entries per api-version with the public key as identifier. - ![img](./image/classdiagramm_x-community-readyness_step2.svg) The 2nd step is an introduction of the entity accounts between the users and the transactions table. This will cause a separation of the transactions from the users, to avoid possible conflicts or dependencies between local users of the community and remote users of foreign users, who will be part of x-communitiy-transactions. - ![img](./image/classdiagramm_x-community-readyness_step3.svg) The 3rd step will introduce an additional foreign-users and a users_favorites table. A foreign_user could be stored in the existing users-table, but he will not need all the attributes of a home-user, especially he will never gets an AGE-account in this community. The user_favorites entity is designed to buildup the relations between users and foreign_users or in general between all users. This is simply a first idea for a future discussion. +After team discussion in architecture meeting a second vision draft for database migration is shown in the following picture. Only the concerned tables of the database migration are presented. The three elliptical surroundings shows the different steps, which should be done in separate issues. The model, table namings and columns are not exhaustive and are still a base for further discussions. + + +![img](./image/class-diagramm_vision-draft2.svg) + +**The first step** with renaming the current `communities` table in `communities_federation` and creating a new `communities` table is not changed. More details about motivation and arguments are described above. + +**The second step** is changed to migrate the `users `table by creating a new `users_settings` table and shift the most existing attributes from `users `table to it. The criterium for shifting a column to `user_settings` is to keep only those columns in the `users `table, which will be used for "home-users" and "foreign-users". A foreign-user at this point of time is a user of an other community, who is involved in a x-community-transaction as `linked_user`. He will not have the possibility to login to the home-community, because after a x-community-tx only the attributes of the `users `table will be exchanged during the handshake of transaction processing of both communities. Even the `transactions `table will be ready for x-community-tx with this `users `and `user_settings` migration, because it contains a reference to both participants of the transaction. For easier displaying and because of historical reasons it will be a good idea to add the columns `linked_user `and `user `(not shown in diagramm) to the `transactions `table with type varchar(512) to store the valid firstname and lastname of the participants at transaction-date. If one of the participants will change his names, there will be no migration of the transaction data necessary and a transaction-list will present always the currect names even over a long distance. + +**The third step** contains a migration for handling accounts and user_correlations. With the new table `gdd_accounts `a new entity will be introduced to support the different types of gradido accounts AGE, GMW and AUF. The second new table `user_correlations `is to buildup the different correlations a user will have: + +* user to user correlation like favorites, trustee, etc +* user to account correlation for cashier of a GMW and AUF community account, trustee of children or seniors, etc. + +The previous idea to replace the `user_id `with an `account_id` in the `transactions `table will be not necessary with this draft, because it will not cause a benefit and saves a lot refactoring efforts. -![img](./image/classdiagramm_communities-communities_federation.png) ##### Community-Entity diff --git a/docu/Concepts/TechnicalRequirements/graphics/classdiagramm_x-community-readyness.drawio b/docu/Concepts/TechnicalRequirements/graphics/classdiagramm_x-community-readyness.drawio index bb6f896c0..becba0b1f 100644 --- a/docu/Concepts/TechnicalRequirements/graphics/classdiagramm_x-community-readyness.drawio +++ b/docu/Concepts/TechnicalRequirements/graphics/classdiagramm_x-community-readyness.drawio @@ -1,38 +1,38 @@ - + - - + + - + - - + + - - + + - + - - + + - - + + - + - - + + - - + + diff --git a/docu/Concepts/TechnicalRequirements/image/class-diagramm_vision-draft2.png b/docu/Concepts/TechnicalRequirements/image/class-diagramm_vision-draft2.png new file mode 100644 index 0000000000000000000000000000000000000000..cd12f1fdde53a69444d34960a6ef8a1cb3ca4c27 GIT binary patch literal 79299 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
X-Community-Readyness
Step 3
X-Community-Read...
X-Community-Readyness
Step 1
X-Community-Read...
X-Community-Readyness
Step 2
X-Community-Read...
Text is not SVG - cannot display \ No newline at end of file From b2f0bf37d2d80e0d714fd97451a1621621d6ca1f Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 30 Mar 2023 16:28:18 +0200 Subject: [PATCH 014/153] Refactor klicktippcontroller to not call function --- backend/src/apis/KlicktippController.ts | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/backend/src/apis/KlicktippController.ts b/backend/src/apis/KlicktippController.ts index a291bb945..3646b174d 100644 --- a/backend/src/apis/KlicktippController.ts +++ b/backend/src/apis/KlicktippController.ts @@ -15,6 +15,9 @@ export const klicktippSignIn = async ( firstName?: string, lastName?: string, ): Promise => { + if (!CONFIG.KLICKTIPP) { + return true + } const fields = { fieldFirstName: firstName, fieldLastName: lastName, @@ -25,12 +28,18 @@ export const klicktippSignIn = async ( } export const signout = async (email: string, language: string): Promise => { + if (!CONFIG.KLICKTIPP) { + return true + } const apiKey = language === 'de' ? CONFIG.KLICKTIPP_APIKEY_DE : CONFIG.KLICKTIPP_APIKEY_EN const result = await klicktippConnector.signoff(apiKey, email) return result } export const unsubscribe = async (email: string): Promise => { + if (!CONFIG.KLICKTIPP) { + return true + } const isLogin = await loginKlicktippUser() if (isLogin) { return await klicktippConnector.unsubscribe(email) @@ -39,6 +48,9 @@ export const unsubscribe = async (email: string): Promise => { } export const getKlickTippUser = async (email: string): Promise => { + if (!CONFIG.KLICKTIPP) { + return true + } const isLogin = await loginKlicktippUser() if (isLogin) { const subscriberId = await klicktippConnector.subscriberSearch(email) @@ -49,14 +61,23 @@ export const getKlickTippUser = async (email: string): Promise => { } export const loginKlicktippUser = async (): Promise => { + if (!CONFIG.KLICKTIPP) { + return true + } return await klicktippConnector.login(CONFIG.KLICKTIPP_USER, CONFIG.KLICKTIPP_PASSWORD) } export const logoutKlicktippUser = async (): Promise => { + if (!CONFIG.KLICKTIPP) { + return true + } return await klicktippConnector.logout() } export const untagUser = async (email: string, tagId: string): Promise => { + if (!CONFIG.KLICKTIPP) { + return true + } const isLogin = await loginKlicktippUser() if (isLogin) { return await klicktippConnector.untag(email, tagId) @@ -65,6 +86,9 @@ export const untagUser = async (email: string, tagId: string): Promise } export const tagUser = async (email: string, tagIds: string): Promise => { + if (!CONFIG.KLICKTIPP) { + return true + } const isLogin = await loginKlicktippUser() if (isLogin) { return await klicktippConnector.tag(email, tagIds) @@ -73,6 +97,9 @@ export const tagUser = async (email: string, tagIds: string): Promise = } export const getKlicktippTagMap = async () => { + if (!CONFIG.KLICKTIPP) { + return '' + } const isLogin = await loginKlicktippUser() if (isLogin) { return await klicktippConnector.tagIndex() From ada9f9a056d15bcbfa99b0adcffd60954c647754 Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 30 Mar 2023 16:28:38 +0200 Subject: [PATCH 015/153] Remove not used function rights. --- backend/src/auth/RIGHTS.ts | 2 -- backend/src/auth/ROLES.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index f643295de..e373a1097 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -5,8 +5,6 @@ export enum RIGHTS { COMMUNITIES = 'COMMUNITIES', LIST_GDT_ENTRIES = 'LIST_GDT_ENTRIES', EXIST_PID = 'EXIST_PID', - GET_KLICKTIPP_USER = 'GET_KLICKTIPP_USER', - GET_KLICKTIPP_TAG_MAP = 'GET_KLICKTIPP_TAG_MAP', UNSUBSCRIBE_NEWSLETTER = 'UNSUBSCRIBE_NEWSLETTER', SUBSCRIBE_NEWSLETTER = 'SUBSCRIBE_NEWSLETTER', TRANSACTION_LIST = 'TRANSACTION_LIST', diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts index 2f3b4e081..d576385e1 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -9,8 +9,6 @@ export const ROLE_USER = new Role('user', [ RIGHTS.BALANCE, RIGHTS.LIST_GDT_ENTRIES, RIGHTS.EXIST_PID, - RIGHTS.GET_KLICKTIPP_USER, - RIGHTS.GET_KLICKTIPP_TAG_MAP, RIGHTS.UNSUBSCRIBE_NEWSLETTER, RIGHTS.SUBSCRIBE_NEWSLETTER, RIGHTS.TRANSACTION_LIST, From 04c6756ef473ba978b838d2bbe826589ea4f8c77 Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 30 Mar 2023 16:29:05 +0200 Subject: [PATCH 016/153] Remove check on CONFIG.KLICKTIPP --- backend/src/middleware/klicktippMiddleware.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/backend/src/middleware/klicktippMiddleware.ts b/backend/src/middleware/klicktippMiddleware.ts index 0469b4ccc..bc11f6ad7 100644 --- a/backend/src/middleware/klicktippMiddleware.ts +++ b/backend/src/middleware/klicktippMiddleware.ts @@ -3,9 +3,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ import { MiddlewareFn } from 'type-graphql' -import { /* klicktippSignIn, */ getKlickTippUser } from '@/apis/KlicktippController' +import { getKlickTippUser } from '@/apis/KlicktippController' import { KlickTipp } from '@model/KlickTipp' -import CONFIG from '@/config' import { klickTippLogger as logger } from '@/server/logger' // export const klicktippRegistrationMiddleware: MiddlewareFn = async ( @@ -28,15 +27,13 @@ export const klicktippNewsletterStateMiddleware: MiddlewareFn = async ( ) => { const result = await next() let klickTipp = new KlickTipp({ status: 'Unsubscribed' }) - if (CONFIG.KLICKTIPP) { - try { - const klickTippUser = await getKlickTippUser(result.email) - if (klickTippUser) { - klickTipp = new KlickTipp(klickTippUser) - } - } catch (err) { - logger.error(`There is no user for (email='${result.email}') ${err}`) + try { + const klickTippUser = await getKlickTippUser(result.email) + if (klickTippUser) { + klickTipp = new KlickTipp(klickTippUser) } + } catch (err) { + logger.error(`There is no user for (email='${result.email}') ${err}`) } result.klickTipp = klickTipp return result From ff7a73048cc84882aea31c85f42b28efa5c7cd8c Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 30 Mar 2023 16:29:35 +0200 Subject: [PATCH 017/153] Correct mutations in testing --- backend/src/seeds/graphql/mutations.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index 1aa12a32f..1523f9621 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -1,14 +1,14 @@ import gql from 'graphql-tag' export const subscribeNewsletter = gql` - mutation ($email: String!, $language: String!) { - subscribeNewsletter(email: $email, language: $language) + mutation { + subscribeNewsletter } ` export const unsubscribeNewsletter = gql` - mutation ($email: String!) { - unsubscribeNewsletter(email: $email) + mutation { + unsubscribeNewsletter } ` From e1c6764978c010f7cd58a4de807c22c5178464c6 Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 30 Mar 2023 16:30:11 +0200 Subject: [PATCH 018/153] Remove unused queries --- .../src/graphql/resolver/KlicktippResolver.ts | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/backend/src/graphql/resolver/KlicktippResolver.ts b/backend/src/graphql/resolver/KlicktippResolver.ts index 19720cc09..1c3438bde 100644 --- a/backend/src/graphql/resolver/KlicktippResolver.ts +++ b/backend/src/graphql/resolver/KlicktippResolver.ts @@ -1,10 +1,6 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -import { Resolver, Query, Authorized, Arg, Mutation, Ctx } from 'type-graphql' +import { Resolver, Authorized, Mutation, Ctx } from 'type-graphql' import { - getKlickTippUser, - getKlicktippTagMap, unsubscribe, klicktippSignIn, } from '@/apis/KlicktippController' @@ -14,18 +10,6 @@ import { Context, getUser } from '@/server/context' @Resolver() export class KlicktippResolver { - @Authorized([RIGHTS.GET_KLICKTIPP_USER]) - @Query(() => String) - async getKlicktippUser(@Arg('email') email: string): Promise { - return await getKlickTippUser(email) - } - - @Authorized([RIGHTS.GET_KLICKTIPP_TAG_MAP]) - @Query(() => String) - async getKlicktippTagMap(): Promise { - return await getKlicktippTagMap() - } - @Authorized([RIGHTS.UNSUBSCRIBE_NEWSLETTER]) @Mutation(() => Boolean) async unsubscribeNewsletter(@Ctx() context: Context): Promise { From 54e912f028faaad20bfb01cb4b712245989059c4 Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 30 Mar 2023 16:30:44 +0200 Subject: [PATCH 019/153] Add unit tests for KlicktippResolver --- .../resolver/KlicktippResolver.test.ts | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 backend/src/graphql/resolver/KlicktippResolver.test.ts diff --git a/backend/src/graphql/resolver/KlicktippResolver.test.ts b/backend/src/graphql/resolver/KlicktippResolver.test.ts new file mode 100644 index 000000000..860444b88 --- /dev/null +++ b/backend/src/graphql/resolver/KlicktippResolver.test.ts @@ -0,0 +1,134 @@ +import { cleanDB, resetToken, testEnvironment } from '@test/helpers' +import { userFactory } from '@/seeds/factory/user' +import { logger, i18n as localization } from '@test/testSetup' +import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' +import { login, subscribeNewsletter, unsubscribeNewsletter } from '@/seeds/graphql/mutations' +import { GraphQLError } from 'graphql' +import { UserContact } from '@entity/UserContact' +import { Event as DbEvent } from '@entity/Event' +import { EventType } from '@/event/Event' + +let testEnv: any, mutate: any, query: any, con: any + +beforeAll(async () => { + testEnv = await testEnvironment(logger, localization) + mutate = testEnv.mutate + query = testEnv.query + con = testEnv.con + await cleanDB() +}) + +afterAll(async () => { + await cleanDB() + await con.close() +}) + +describe('KlicktippResolver', () => { + let bibi + + beforeAll(async () => { + bibi = await userFactory(testEnv, bibiBloxberg) + }) + + afterAll(async () => { + await cleanDB() + }) + + describe('subscribeNewsletter', () => { + describe('unauthenticated', () => { + it('returns an error', async () => { + const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({ + mutation: subscribeNewsletter, + }) + + expect(errorObjects).toEqual([new GraphQLError('401 Unauthorized')]) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(() => { + resetToken() + }) + + it('calls API', async () => { + const { + data: { subscribeNewsletter: isSubscribed }, + }: { data: { subscribeNewsletter: boolean } } = await mutate({ + mutation: subscribeNewsletter, + }) + + expect(isSubscribed).toEqual(true) + }) + + it('stores the SUBSCRIBE_NEWSLETTER event in the database', async () => { + const userConatct = await UserContact.findOneOrFail( + { email: 'bibi@bloxberg.de' }, + { relations: ['user'] }, + ) + await expect(DbEvent.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventType.SUBSCRIBE_NEWSLETTER, + affectedUserId: userConatct.user.id, + actingUserId: userConatct.user.id, + }), + ) + }) + }) + }) + + describe('unsubscribeNewsletter', () => { + describe('unauthenticated', () => { + it('returns an error', async () => { + const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({ + mutation: unsubscribeNewsletter, + }) + + expect(errorObjects).toEqual([new GraphQLError('401 Unauthorized')]) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(() => { + resetToken() + }) + + it('calls API', async () => { + const { + data: { unsubscribeNewsletter: isUnsubscribed }, + }: { data: { unsubscribeNewsletter: boolean } } = await mutate({ + mutation: unsubscribeNewsletter, + }) + + expect(isUnsubscribed).toEqual(true) + }) + + it('stores the UNSUBSCRIBE_NEWSLETTER event in the database', async () => { + const userConatct = await UserContact.findOneOrFail( + { email: 'bibi@bloxberg.de' }, + { relations: ['user'] }, + ) + await expect(DbEvent.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventType.UNSUBSCRIBE_NEWSLETTER, + affectedUserId: userConatct.user.id, + actingUserId: userConatct.user.id, + }), + ) + }) + }) + }) +}) From 8c3a847af6bf84a471def4f579fc404e189a50de Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 30 Mar 2023 16:31:02 +0200 Subject: [PATCH 020/153] Linting --- backend/src/graphql/resolver/KlicktippResolver.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/backend/src/graphql/resolver/KlicktippResolver.ts b/backend/src/graphql/resolver/KlicktippResolver.ts index 1c3438bde..e03e81440 100644 --- a/backend/src/graphql/resolver/KlicktippResolver.ts +++ b/backend/src/graphql/resolver/KlicktippResolver.ts @@ -1,9 +1,6 @@ import { Resolver, Authorized, Mutation, Ctx } from 'type-graphql' -import { - unsubscribe, - klicktippSignIn, -} from '@/apis/KlicktippController' +import { unsubscribe, klicktippSignIn } from '@/apis/KlicktippController' import { EVENT_UNSUBSCRIBE_NEWSLETTER, EVENT_SUBSCRIBE_NEWSLETTER } from '@/event/Event' import { RIGHTS } from '@/auth/RIGHTS' import { Context, getUser } from '@/server/context' From c07582d5a1a55518433a313716fb965f148e305b Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 30 Mar 2023 16:44:17 +0200 Subject: [PATCH 021/153] Merge & linting --- .../src/emails/sendEmailTranslated.test.ts | 2 +- backend/src/emails/sendEmailVariants.test.ts | 4 ++-- backend/src/event/Event.ts | 2 +- .../federation/validateCommunities.test.ts | 2 +- backend/src/graphql/model/Transaction.ts | 2 +- .../src/graphql/resolver/BalanceResolver.ts | 5 ++-- .../resolver/CommunityResolver.test.ts | 2 +- .../resolver/ContributionLinkResolver.test.ts | 2 +- .../resolver/ContributionLinkResolver.ts | 14 +++++------ .../resolver/ContributionResolver.test.ts | 24 +++++++++---------- .../graphql/resolver/ContributionResolver.ts | 20 ++++++++-------- .../resolver/KlicktippResolver.test.ts | 6 ++--- .../resolver/TransactionLinkResolver.test.ts | 6 ++--- .../resolver/TransactionLinkResolver.ts | 10 ++++---- .../resolver/TransactionResolver.test.ts | 4 ++-- .../graphql/resolver/TransactionResolver.ts | 10 ++++---- .../src/graphql/resolver/UserResolver.test.ts | 4 ++-- backend/src/graphql/resolver/UserResolver.ts | 17 +++++++------ .../src/graphql/resolver/semaphore.test.ts | 2 +- .../graphql/resolver/util/creations.test.ts | 2 +- .../src/graphql/resolver/util/creations.ts | 2 +- backend/src/middleware/klicktippMiddleware.ts | 2 +- backend/src/password/EncryptorUtils.ts | 2 +- backend/src/seeds/factory/contributionLink.ts | 2 +- backend/src/server/LogError.test.ts | 2 +- backend/src/util/communityUser.ts | 2 +- backend/src/util/decay.ts | 2 +- backend/src/util/validate.ts | 4 ++-- backend/src/util/virtualTransactions.ts | 2 +- e2e-tests/yarn.lock | 10 ++++---- 30 files changed, 84 insertions(+), 86 deletions(-) diff --git a/backend/src/emails/sendEmailTranslated.test.ts b/backend/src/emails/sendEmailTranslated.test.ts index 762b88cf0..f2fae2746 100644 --- a/backend/src/emails/sendEmailTranslated.test.ts +++ b/backend/src/emails/sendEmailTranslated.test.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/unbound-method */ import { createTransport } from 'nodemailer' -import { sendEmailTranslated } from './sendEmailTranslated' import { logger, i18n } from '@test/testSetup' +import { sendEmailTranslated } from './sendEmailTranslated' import CONFIG from '@/config' CONFIG.EMAIL = false diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index 399ed89ac..3c8d22f95 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -4,6 +4,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { Decimal } from 'decimal.js-light' +import { testEnvironment } from '@test/helpers' +import { logger, i18n as localization } from '@test/testSetup' import { sendAddedContributionMessageEmail, sendAccountActivationEmail, @@ -16,8 +18,6 @@ import { sendTransactionReceivedEmail, } from './sendEmailVariants' import { sendEmailTranslated } from './sendEmailTranslated' -import { testEnvironment } from '@test/helpers' -import { logger, i18n as localization } from '@test/testSetup' import CONFIG from '@/config' let con: any diff --git a/backend/src/event/Event.ts b/backend/src/event/Event.ts index bfeba6d50..e2a5c0090 100644 --- a/backend/src/event/Event.ts +++ b/backend/src/event/Event.ts @@ -57,7 +57,7 @@ export { EVENT_EMAIL_ACCOUNT_MULTIREGISTRATION } from './EVENT_EMAIL_ACCOUNT_MUL export { EVENT_EMAIL_ADMIN_CONFIRMATION } from './EVENT_EMAIL_ADMIN_CONFIRMATION' export { EVENT_EMAIL_CONFIRMATION } from './EVENT_EMAIL_CONFIRMATION' export { EVENT_EMAIL_FORGOT_PASSWORD } from './EVENT_EMAIL_FORGOT_PASSWORD' -export { EVENT_SEND_CONFIRMATION_EMAIL } from './EVENT_SEND_CONFIRMATION_EMAIL' +export { EVENT_SUBSCRIBE_NEWSLETTER } from './EVENT_SUBSCRIBE_NEWSLETTER' export { EVENT_TRANSACTION_SEND } from './EVENT_TRANSACTION_SEND' export { EVENT_TRANSACTION_RECEIVE } from './EVENT_TRANSACTION_RECEIVE' export { EVENT_TRANSACTION_LINK_CREATE } from './EVENT_TRANSACTION_LINK_CREATE' diff --git a/backend/src/federation/validateCommunities.test.ts b/backend/src/federation/validateCommunities.test.ts index a99bb3274..57d9bd65f 100644 --- a/backend/src/federation/validateCommunities.test.ts +++ b/backend/src/federation/validateCommunities.test.ts @@ -6,9 +6,9 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { Community as DbCommunity } from '@entity/Community' -import { validateCommunities } from './validateCommunities' import { logger } from '@test/testSetup' import { testEnvironment, cleanDB } from '@test/helpers' +import { validateCommunities } from './validateCommunities' let con: any let testEnv: any diff --git a/backend/src/graphql/model/Transaction.ts b/backend/src/graphql/model/Transaction.ts index 8f0d1eadc..9e2be85dd 100644 --- a/backend/src/graphql/model/Transaction.ts +++ b/backend/src/graphql/model/Transaction.ts @@ -1,9 +1,9 @@ import { ObjectType, Field, Int } from 'type-graphql' import { Transaction as dbTransaction } from '@entity/Transaction' import { Decimal } from 'decimal.js-light' +import { TransactionTypeId } from '@enum/TransactionTypeId' import { Decay } from './Decay' import { User } from './User' -import { TransactionTypeId } from '@enum/TransactionTypeId' @ObjectType() export class Transaction { diff --git a/backend/src/graphql/resolver/BalanceResolver.ts b/backend/src/graphql/resolver/BalanceResolver.ts index 7600f12b9..520b8c094 100644 --- a/backend/src/graphql/resolver/BalanceResolver.ts +++ b/backend/src/graphql/resolver/BalanceResolver.ts @@ -6,11 +6,10 @@ import { getCustomRepository } from '@dbTools/typeorm' import { Transaction as dbTransaction } from '@entity/Transaction' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' +import { TransactionLinkRepository } from '@repository/TransactionLink' +import { Balance } from '@model/Balance' import { GdtResolver } from './GdtResolver' import { getLastTransaction } from './util/getLastTransaction' -import { TransactionLinkRepository } from '@repository/TransactionLink' - -import { Balance } from '@model/Balance' import { backendLogger as logger } from '@/server/logger' import { Context, getUser } from '@/server/context' diff --git a/backend/src/graphql/resolver/CommunityResolver.test.ts b/backend/src/graphql/resolver/CommunityResolver.test.ts index f4352c095..5513a73b8 100644 --- a/backend/src/graphql/resolver/CommunityResolver.test.ts +++ b/backend/src/graphql/resolver/CommunityResolver.test.ts @@ -6,8 +6,8 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { Community as DbCommunity } from '@entity/Community' -import { getCommunities } from '@/seeds/graphql/queries' import { testEnvironment } from '@test/helpers' +import { getCommunities } from '@/seeds/graphql/queries' let query: any diff --git a/backend/src/graphql/resolver/ContributionLinkResolver.test.ts b/backend/src/graphql/resolver/ContributionLinkResolver.test.ts index 6a69e257e..7dfb44e55 100644 --- a/backend/src/graphql/resolver/ContributionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionLinkResolver.test.ts @@ -9,6 +9,7 @@ import { GraphQLError } from 'graphql' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' import { Event as DbEvent } from '@entity/Event' import { logger } from '@test/testSetup' +import { cleanDB, testEnvironment, resetToken } from '@test/helpers' import { login, createContributionLink, @@ -16,7 +17,6 @@ import { updateContributionLink, } from '@/seeds/graphql/mutations' import { listContributionLinks } from '@/seeds/graphql/queries' -import { cleanDB, testEnvironment, resetToken } from '@test/helpers' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { peterLustig } from '@/seeds/users/peter-lustig' import { userFactory } from '@/seeds/factory/user' diff --git a/backend/src/graphql/resolver/ContributionLinkResolver.ts b/backend/src/graphql/resolver/ContributionLinkResolver.ts index 55a23187f..dc88bea07 100644 --- a/backend/src/graphql/resolver/ContributionLinkResolver.ts +++ b/backend/src/graphql/resolver/ContributionLinkResolver.ts @@ -3,20 +3,20 @@ import { Resolver, Args, Arg, Authorized, Mutation, Query, Int, Ctx } from 'type import { MoreThan, IsNull } from '@dbTools/typeorm' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' +import { ContributionLinkList } from '@model/ContributionLinkList' +import { ContributionLink } from '@model/ContributionLink' +import ContributionLinkArgs from '@arg/ContributionLinkArgs' +import { Order } from '@enum/Order' +import Paginated from '@arg/Paginated' +import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' +import { isStartEndDateValid } from './util/creations' import { CONTRIBUTIONLINK_NAME_MAX_CHARS, CONTRIBUTIONLINK_NAME_MIN_CHARS, MEMO_MAX_CHARS, MEMO_MIN_CHARS, } from './const/const' -import { isStartEndDateValid } from './util/creations' -import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' -import { ContributionLinkList } from '@model/ContributionLinkList' -import { ContributionLink } from '@model/ContributionLink' -import ContributionLinkArgs from '@arg/ContributionLinkArgs' import { RIGHTS } from '@/auth/RIGHTS' -import { Order } from '@enum/Order' -import Paginated from '@arg/Paginated' // TODO: this is a strange construct import LogError from '@/server/LogError' diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index df8524a0c..1dfb6567c 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -13,6 +13,18 @@ import { Transaction as DbTransaction } from '@entity/Transaction' import { User } from '@entity/User' import { UserInputError } from 'apollo-server-express' import { Event as DbEvent } from '@entity/Event' +import { + cleanDB, + resetToken, + testEnvironment, + contributionDateFormatter, + resetEntity, +} from '@test/helpers' +import { logger, i18n as localization } from '@test/testSetup' +import { UnconfirmedContribution } from '@model/UnconfirmedContribution' +import { ContributionListResult } from '@model/Contribution' +import { ContributionStatus } from '@enum/ContributionStatus' +import { Order } from '@enum/Order' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { stephenHawking } from '@/seeds/users/stephen-hawking' @@ -40,24 +52,12 @@ import { sendContributionDeletedEmail, sendContributionDeniedEmail, } from '@/emails/sendEmailVariants' -import { - cleanDB, - resetToken, - testEnvironment, - contributionDateFormatter, - resetEntity, -} from '@test/helpers' import { userFactory } from '@/seeds/factory/user' import { creationFactory } from '@/seeds/factory/creation' import { creations } from '@/seeds/creation/index' import { peterLustig } from '@/seeds/users/peter-lustig' import { EventType } from '@/event/Event' -import { logger, i18n as localization } from '@test/testSetup' import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz' -import { UnconfirmedContribution } from '@model/UnconfirmedContribution' -import { ContributionListResult } from '@model/Contribution' -import { ContributionStatus } from '@enum/ContributionStatus' -import { Order } from '@enum/Order' jest.mock('@/emails/sendEmailVariants') diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 83cf29fc7..6e412fc72 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -9,16 +9,6 @@ import { UserContact } from '@entity/UserContact' import { User as DbUser } from '@entity/User' import { Transaction as DbTransaction } from '@entity/Transaction' -import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' -import { getLastTransaction } from './util/getLastTransaction' -import { findContributions } from './util/findContributions' -import { - getUserCreation, - validateContribution, - updateCreations, - isValidDateString, - getOpenCreations, -} from './util/creations' import { AdminUpdateContribution } from '@model/AdminUpdateContribution' import { Contribution, ContributionListResult } from '@model/Contribution' import { Decay } from '@model/Decay' @@ -33,6 +23,16 @@ import ContributionArgs from '@arg/ContributionArgs' import Paginated from '@arg/Paginated' import AdminCreateContributionArgs from '@arg/AdminCreateContributionArgs' import AdminUpdateContributionArgs from '@arg/AdminUpdateContributionArgs' +import { + getUserCreation, + validateContribution, + updateCreations, + isValidDateString, + getOpenCreations, +} from './util/creations' +import { findContributions } from './util/findContributions' +import { getLastTransaction } from './util/getLastTransaction' +import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { RIGHTS } from '@/auth/RIGHTS' import { Context, getUser, getClientTimezoneOffset } from '@/server/context' diff --git a/backend/src/graphql/resolver/KlicktippResolver.test.ts b/backend/src/graphql/resolver/KlicktippResolver.test.ts index 860444b88..0e64a779f 100644 --- a/backend/src/graphql/resolver/KlicktippResolver.test.ts +++ b/backend/src/graphql/resolver/KlicktippResolver.test.ts @@ -1,11 +1,11 @@ import { cleanDB, resetToken, testEnvironment } from '@test/helpers' -import { userFactory } from '@/seeds/factory/user' import { logger, i18n as localization } from '@test/testSetup' -import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' -import { login, subscribeNewsletter, unsubscribeNewsletter } from '@/seeds/graphql/mutations' import { GraphQLError } from 'graphql' import { UserContact } from '@entity/UserContact' import { Event as DbEvent } from '@entity/Event' +import { login, subscribeNewsletter, unsubscribeNewsletter } from '@/seeds/graphql/mutations' +import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' +import { userFactory } from '@/seeds/factory/user' import { EventType } from '@/event/Event' let testEnv: any, mutate: any, query: any, con: any diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts index fd2a44b4b..96eb99ea7 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts @@ -13,10 +13,12 @@ import { GraphQLError } from 'graphql' import { Transaction } from '@entity/Transaction' import { Event as DbEvent } from '@entity/Event' import { UserContact } from '@entity/UserContact' +import { cleanDB, testEnvironment, resetToken, resetEntity } from '@test/helpers' +import { UnconfirmedContribution } from '@model/UnconfirmedContribution' +import { logger } from '@test/testSetup' import { transactionLinkCode } from './TransactionLinkResolver' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { peterLustig } from '@/seeds/users/peter-lustig' -import { cleanDB, testEnvironment, resetToken, resetEntity } from '@test/helpers' import { creationFactory } from '@/seeds/factory/creation' import { creations } from '@/seeds/creation/index' import { userFactory } from '@/seeds/factory/user' @@ -33,9 +35,7 @@ import { confirmContribution, } from '@/seeds/graphql/mutations' import { listTransactionLinksAdmin } from '@/seeds/graphql/queries' -import { UnconfirmedContribution } from '@model/UnconfirmedContribution' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' -import { logger } from '@test/testSetup' import { EventType } from '@/event/Event' // mock semaphore to allow use fake timers diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 6aa829ac1..534db02c1 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -10,10 +10,6 @@ import { Contribution as DbContribution } from '@entity/Contribution' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' import { Resolver, Args, Arg, Authorized, Ctx, Mutation, Query, Int } from 'type-graphql' -import { getUserCreation, validateContribution } from './util/creations' -import { executeTransaction } from './TransactionResolver' -import { getLastTransaction } from './util/getLastTransaction' -import transactionLinkList from './util/transactionLinkList' import { User } from '@model/User' import { ContributionLink } from '@model/ContributionLink' import { Decay } from '@model/Decay' @@ -25,12 +21,16 @@ import { ContributionCycleType } from '@enum/ContributionCycleType' import TransactionLinkArgs from '@arg/TransactionLinkArgs' import Paginated from '@arg/Paginated' import TransactionLinkFilters from '@arg/TransactionLinkFilters' +import QueryLinkResult from '@union/QueryLinkResult' +import transactionLinkList from './util/transactionLinkList' +import { getLastTransaction } from './util/getLastTransaction' +import { executeTransaction } from './TransactionResolver' +import { getUserCreation, validateContribution } from './util/creations' import { backendLogger as logger } from '@/server/logger' import { Context, getUser, getClientTimezoneOffset } from '@/server/context' import { calculateBalance } from '@/util/validate' import { RIGHTS } from '@/auth/RIGHTS' import { calculateDecay } from '@/util/decay' -import QueryLinkResult from '@union/QueryLinkResult' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import LogError from '@/server/LogError' import { diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index f26234363..17ed81676 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -10,6 +10,8 @@ import { Transaction } from '@entity/Transaction' import { User } from '@entity/User' import { GraphQLError } from 'graphql' import { Event as DbEvent } from '@entity/Event' +import { cleanDB, testEnvironment } from '@test/helpers' +import { logger } from '@test/testSetup' import { findUserByEmail } from './UserResolver' import { EventType } from '@/event/Event' import { userFactory } from '@/seeds/factory/user' @@ -23,8 +25,6 @@ import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { garrickOllivander } from '@/seeds/users/garrick-ollivander' import { peterLustig } from '@/seeds/users/peter-lustig' import { stephenHawking } from '@/seeds/users/stephen-hawking' -import { cleanDB, testEnvironment } from '@test/helpers' -import { logger } from '@test/testSetup' let mutate: any, query: any, con: any let testEnv: any diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index f38a4a07b..e0ac5d435 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -9,10 +9,6 @@ import { getCustomRepository, getConnection, In } from '@dbTools/typeorm' import { User as dbUser } from '@entity/User' import { Transaction as dbTransaction } from '@entity/Transaction' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' -import { BalanceResolver } from './BalanceResolver' -import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' -import { findUserByEmail } from './UserResolver' -import { getLastTransaction } from './util/getLastTransaction' import { TransactionRepository } from '@repository/Transaction' import { TransactionLinkRepository } from '@repository/TransactionLink' @@ -21,9 +17,13 @@ import { Transaction } from '@model/Transaction' import { TransactionList } from '@model/TransactionList' import { Order } from '@enum/Order' import { TransactionTypeId } from '@enum/TransactionTypeId' -import { calculateBalance } from '@/util/validate' import TransactionSendArgs from '@arg/TransactionSendArgs' import Paginated from '@arg/Paginated' +import { getLastTransaction } from './util/getLastTransaction' +import { findUserByEmail } from './UserResolver' +import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' +import { BalanceResolver } from './BalanceResolver' +import { calculateBalance } from '@/util/validate' import { backendLogger as logger } from '@/server/logger' import { Context, getUser } from '@/server/context' diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index aebd0f0eb..1b6239fea 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -15,9 +15,10 @@ import { Event as DbEvent } from '@entity/Event' import { OptInType } from '@enum/OptInType' import { UserContactType } from '@enum/UserContactType' import { PasswordEncryptionType } from '@enum/PasswordEncryptionType' -import { objectValuesToArray } from '@/util/utilities' import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers' import { logger, i18n as localization } from '@test/testSetup' +import { ContributionLink } from '@model/ContributionLink' +import { objectValuesToArray } from '@/util/utilities' import { printTimeDuration } from '@/util/time' import { userFactory } from '@/seeds/factory/user' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' @@ -44,7 +45,6 @@ import { } from '@/emails/sendEmailVariants' import { contributionLinkFactory } from '@/seeds/factory/contributionLink' import { transactionLinkFactory } from '@/seeds/factory/transactionLink' -import { ContributionLink } from '@model/ContributionLink' import { EventType } from '@/event/Event' import { peterLustig } from '@/seeds/users/peter-lustig' import { bobBaumeister } from '@/seeds/users/bob-baumeister' diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 54d4f583f..aca72b5d0 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -21,8 +21,6 @@ import { User as DbUser } from '@entity/User' import { UserContact as DbUserContact } from '@entity/UserContact' import { TransactionLink as DbTransactionLink } from '@entity/TransactionLink' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' -import { getUserCreations } from './util/creations' -import { FULL_CREATION_AVAILABLE } from './const/const' import { PasswordEncryptionType } from '@enum/PasswordEncryptionType' import { UserRepository } from '@repository/User' @@ -33,18 +31,19 @@ import { OptInType } from '@enum/OptInType' import { Order } from '@enum/Order' import { UserContactType } from '@enum/UserContactType' -import { - sendAccountActivationEmail, - sendAccountMultiRegistrationEmail, - sendResetPasswordEmail, -} from '@/emails/sendEmailVariants' - -import { getTimeDurationObject, printTimeDuration } from '@/util/time' import CreateUserArgs from '@arg/CreateUserArgs' import UnsecureLoginArgs from '@arg/UnsecureLoginArgs' import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs' import Paginated from '@arg/Paginated' import SearchUsersArgs from '@arg/SearchUsersArgs' +import { FULL_CREATION_AVAILABLE } from './const/const' +import { getUserCreations } from './util/creations' +import { getTimeDurationObject, printTimeDuration } from '@/util/time' +import { + sendAccountActivationEmail, + sendAccountMultiRegistrationEmail, + sendResetPasswordEmail, +} from '@/emails/sendEmailVariants' import { backendLogger as logger } from '@/server/logger' import { Context, getUser, getClientTimezoneOffset } from '@/server/context' diff --git a/backend/src/graphql/resolver/semaphore.test.ts b/backend/src/graphql/resolver/semaphore.test.ts index 6b1976021..cc4d589dc 100644 --- a/backend/src/graphql/resolver/semaphore.test.ts +++ b/backend/src/graphql/resolver/semaphore.test.ts @@ -5,12 +5,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Decimal } from 'decimal.js-light' +import { cleanDB, testEnvironment, contributionDateFormatter } from '@test/helpers' 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 { creationFactory, nMonthsBefore } from '@/seeds/factory/creation' -import { cleanDB, testEnvironment, contributionDateFormatter } from '@test/helpers' import { confirmContribution, createContribution, diff --git a/backend/src/graphql/resolver/util/creations.test.ts b/backend/src/graphql/resolver/util/creations.test.ts index 7461401c8..5b6b40e2b 100644 --- a/backend/src/graphql/resolver/util/creations.test.ts +++ b/backend/src/graphql/resolver/util/creations.test.ts @@ -6,8 +6,8 @@ import { User } from '@entity/User' import { Contribution } from '@entity/Contribution' -import { getUserCreation } from './creations' import { testEnvironment, cleanDB, contributionDateFormatter } from '@test/helpers' +import { getUserCreation } from './creations' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { peterLustig } from '@/seeds/users/peter-lustig' import { userFactory } from '@/seeds/factory/user' diff --git a/backend/src/graphql/resolver/util/creations.ts b/backend/src/graphql/resolver/util/creations.ts index 6ebeae8b9..dba0c8c81 100644 --- a/backend/src/graphql/resolver/util/creations.ts +++ b/backend/src/graphql/resolver/util/creations.ts @@ -3,9 +3,9 @@ import { getConnection } from '@dbTools/typeorm' import { Contribution } from '@entity/Contribution' import { Decimal } from 'decimal.js-light' +import { OpenCreation } from '@model/OpenCreation' import { FULL_CREATION_AVAILABLE, MAX_CREATION_AMOUNT } from '@/graphql/resolver/const/const' import { backendLogger as logger } from '@/server/logger' -import { OpenCreation } from '@model/OpenCreation' import LogError from '@/server/LogError' interface CreationMap { diff --git a/backend/src/middleware/klicktippMiddleware.ts b/backend/src/middleware/klicktippMiddleware.ts index bc11f6ad7..369b29475 100644 --- a/backend/src/middleware/klicktippMiddleware.ts +++ b/backend/src/middleware/klicktippMiddleware.ts @@ -3,8 +3,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ import { MiddlewareFn } from 'type-graphql' -import { getKlickTippUser } from '@/apis/KlicktippController' import { KlickTipp } from '@model/KlickTipp' +import { getKlickTippUser } from '@/apis/KlicktippController' import { klickTippLogger as logger } from '@/server/logger' // export const klicktippRegistrationMiddleware: MiddlewareFn = async ( diff --git a/backend/src/password/EncryptorUtils.ts b/backend/src/password/EncryptorUtils.ts index b4531b3bb..ab8a333d2 100644 --- a/backend/src/password/EncryptorUtils.ts +++ b/backend/src/password/EncryptorUtils.ts @@ -2,10 +2,10 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { User } from '@entity/User' +import { PasswordEncryptionType } from '@enum/PasswordEncryptionType' import CONFIG from '@/config' import LogError from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' -import { PasswordEncryptionType } from '@enum/PasswordEncryptionType' // eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs const sodium = require('sodium-native') diff --git a/backend/src/seeds/factory/contributionLink.ts b/backend/src/seeds/factory/contributionLink.ts index 5925cdcfe..6e1d9bd50 100644 --- a/backend/src/seeds/factory/contributionLink.ts +++ b/backend/src/seeds/factory/contributionLink.ts @@ -2,8 +2,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/unbound-method */ import { ApolloServerTestClient } from 'apollo-server-testing' -import { login, createContributionLink } from '@/seeds/graphql/mutations' import { ContributionLink } from '@model/ContributionLink' +import { login, createContributionLink } from '@/seeds/graphql/mutations' import { ContributionLinkInterface } from '@/seeds/contributionLink/ContributionLinkInterface' export const contributionLinkFactory = async ( diff --git a/backend/src/server/LogError.test.ts b/backend/src/server/LogError.test.ts index 318a477ef..b7a067e28 100644 --- a/backend/src/server/LogError.test.ts +++ b/backend/src/server/LogError.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/unbound-method */ -import LogError from './LogError' import { logger } from '@test/testSetup' +import LogError from './LogError' describe('LogError', () => { it('logs an Error when created', () => { diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index dfa477da9..d086727bf 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -3,9 +3,9 @@ import { SaveOptions, RemoveOptions } from '@dbTools/typeorm' import { User as dbUser } from '@entity/User' import { UserContact } from '@entity/UserContact' +import { User } from '@model/User' import { PasswordEncryptionType } from '@/graphql/enum/PasswordEncryptionType' // import { UserContact as EmailContact } from '@entity/UserContact' -import { User } from '@model/User' const communityDbUser: dbUser = { id: -1, diff --git a/backend/src/util/decay.ts b/backend/src/util/decay.ts index d35eb83a4..3c76b0995 100644 --- a/backend/src/util/decay.ts +++ b/backend/src/util/decay.ts @@ -1,6 +1,6 @@ import { Decimal } from 'decimal.js-light' -import CONFIG from '@/config' import { Decay } from '@model/Decay' +import CONFIG from '@/config' import LogError from '@/server/LogError' // TODO: externalize all those definitions and functions into an external decay library diff --git a/backend/src/util/validate.ts b/backend/src/util/validate.ts index ec28dfa13..b70c90344 100644 --- a/backend/src/util/validate.ts +++ b/backend/src/util/validate.ts @@ -1,10 +1,10 @@ import { Decimal } from 'decimal.js-light' import { getCustomRepository } from '@dbTools/typeorm' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' -import { calculateDecay } from './decay' -import { getLastTransaction } from '@/graphql/resolver/util/getLastTransaction' import { TransactionLinkRepository } from '@repository/TransactionLink' import { Decay } from '@model/Decay' +import { calculateDecay } from './decay' +import { getLastTransaction } from '@/graphql/resolver/util/getLastTransaction' function isStringBoolean(value: string): boolean { const lowerValue = value.toLowerCase() diff --git a/backend/src/util/virtualTransactions.ts b/backend/src/util/virtualTransactions.ts index 7810ad871..aea4ebb66 100644 --- a/backend/src/util/virtualTransactions.ts +++ b/backend/src/util/virtualTransactions.ts @@ -2,10 +2,10 @@ import { SaveOptions, RemoveOptions } from '@dbTools/typeorm' import { Transaction as dbTransaction } from '@entity/Transaction' import { Decimal } from 'decimal.js-light' -import { calculateDecay } from './decay' import { Transaction } from '@model/Transaction' import { TransactionTypeId } from '@enum/TransactionTypeId' import { User } from '@model/User' +import { calculateDecay } from './decay' const defaultModelFunctions = { hasId: function (): boolean { diff --git a/e2e-tests/yarn.lock b/e2e-tests/yarn.lock index c0f623e47..20956c5d0 100644 --- a/e2e-tests/yarn.lock +++ b/e2e-tests/yarn.lock @@ -2193,10 +2193,10 @@ crypto-browserify@^3.0.0: randombytes "^2.0.0" randomfill "^1.0.3" -cypress@^10.4.0: - version "10.8.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-10.8.0.tgz#12a681f2642b6f13d636bab65d5b71abdb1497a5" - integrity sha512-QVse0dnLm018hgti2enKMVZR9qbIO488YGX06nH5j3Dg1isL38DwrBtyrax02CANU6y8F4EJUuyW6HJKw1jsFA== +cypress@^12.7.0: + version "12.9.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.9.0.tgz#e6ab43cf329fd7c821ef7645517649d72ccf0a12" + integrity sha512-Ofe09LbHKgSqX89Iy1xen2WvpgbvNxDzsWx3mgU1mfILouELeXYGwIib3ItCwoRrRifoQwcBFmY54Vs0zw7QCg== dependencies: "@cypress/request" "^2.88.10" "@cypress/xvfb" "^1.2.4" @@ -2215,7 +2215,7 @@ cypress@^10.4.0: commander "^5.1.0" common-tags "^1.8.0" dayjs "^1.10.4" - debug "^4.3.2" + debug "^4.3.4" enquirer "^2.3.6" eventemitter2 "6.4.7" execa "4.1.0" From 2435afcd7caaea9c039bcf313d2c810d5e685b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 31 Mar 2023 00:31:55 +0200 Subject: [PATCH 022/153] 3rd draft of database modell --- .../TechnicalRequirements/Federation.md | 15 +++++++++- ...classdiagramm_x-community-readyness.drawio | 28 +++++++++++++----- .../image/class-diagramm_vision-draft3.png | Bin 0 -> 95513 bytes .../image/class-diagramm_vision-draft3.svg | 1 + 4 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 docu/Concepts/TechnicalRequirements/image/class-diagramm_vision-draft3.png create mode 100644 docu/Concepts/TechnicalRequirements/image/class-diagramm_vision-draft3.svg diff --git a/docu/Concepts/TechnicalRequirements/Federation.md b/docu/Concepts/TechnicalRequirements/Federation.md index 2ab4272f2..84c4965af 100644 --- a/docu/Concepts/TechnicalRequirements/Federation.md +++ b/docu/Concepts/TechnicalRequirements/Federation.md @@ -50,6 +50,8 @@ Before starting in describing the details of the federation handshake, some prer With the federation additional data tables/entities have to be created. +##### 1st Draft + The following diagramms shows the first draft of the possible database-model base on the migration 0063-event_link_fields.ts with 3 steps of migration to reach the required entities. All three diagramms are not exhaustive and are still a base for discussions: ![img](./image/classdiagramm_x-community-readyness_step1.svg) @@ -64,10 +66,10 @@ The 2nd step is an introduction of the entity accounts between the users and the The 3rd step will introduce an additional foreign-users and a users_favorites table. A foreign_user could be stored in the existing users-table, but he will not need all the attributes of a home-user, especially he will never gets an AGE-account in this community. The user_favorites entity is designed to buildup the relations between users and foreign_users or in general between all users. This is simply a first idea for a future discussion. +##### 2nd Draft After team discussion in architecture meeting a second vision draft for database migration is shown in the following picture. Only the concerned tables of the database migration are presented. The three elliptical surroundings shows the different steps, which should be done in separate issues. The model, table namings and columns are not exhaustive and are still a base for further discussions. - ![img](./image/class-diagramm_vision-draft2.svg) **The first step** with renaming the current `communities` table in `communities_federation` and creating a new `communities` table is not changed. More details about motivation and arguments are described above. @@ -81,6 +83,17 @@ After team discussion in architecture meeting a second vision draft for database The previous idea to replace the `user_id `with an `account_id` in the `transactions `table will be not necessary with this draft, because it will not cause a benefit and saves a lot refactoring efforts. +##### 3rd Draft + +After further discussions about the database-model and the necessary migration steps the team decided to integrate an additional migration step for X-Community-Transaction. The decision base on keeping the goal focus on implementation of the x-community sendCoins feature as soon as possible. + +![img](./image/class-diagramm_vision-draft3.svg) + +The additional migration step 2 will simply concentrated on the `transactions `table by adding all necessary columns to handle a *x-community-tx* without a previous migration of the `users `table, shown as step 3 in the picture above. + +In concequence of these additional columns in the `transactions `table the database-model will be denormalized by containing redundanten columns. But this migration step will reduce the necessary efforts to reach the *x-community sendCoins* feature as soon as possible. On the other side it offers more possibilities to ensure data consitency, because of internal data checks in conjunction with the redundant columns. + +The feature *x-community-tx* per *sendCoins* will create several challenges, because there is no technical transaction bracket, which will ensure consistent data in both community databases after all write access are finished. The most favorite concept about handling a x-community-transaction and the upcoming handshake to ensure valid send- and receive-transaction entries in both databases is to follow the two-phase-commit protocol. To avoid blocking the transactions table or user dependent transactions-entries during the two-phase-commit processing the idea of pending_transactions is born. This additional pending_transactions table contains the same columns than the transactions table plus one column named `x_transactions_state`. ##### Community-Entity diff --git a/docu/Concepts/TechnicalRequirements/graphics/classdiagramm_x-community-readyness.drawio b/docu/Concepts/TechnicalRequirements/graphics/classdiagramm_x-community-readyness.drawio index becba0b1f..c618971f8 100644 --- a/docu/Concepts/TechnicalRequirements/graphics/classdiagramm_x-community-readyness.drawio +++ b/docu/Concepts/TechnicalRequirements/graphics/classdiagramm_x-community-readyness.drawio @@ -4,17 +4,17 @@ - - + + - + - - + + - - + + @@ -28,12 +28,24 @@ - + + + + + + + + + + + + +
diff --git a/docu/Concepts/TechnicalRequirements/image/class-diagramm_vision-draft3.png b/docu/Concepts/TechnicalRequirements/image/class-diagramm_vision-draft3.png new file mode 100644 index 0000000000000000000000000000000000000000..b6643fd32e573e3ba713bd54caaaf623334b4a63 GIT binary patch literal 95513 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

X-Community-Readyness
Step 4
X-Community-Read...
X-Community-Readyness
Step 1
X-Community-Read...
X-Community-Readyness
Step 3
X-Community-Read...
X-Community-Readyness
Step 2
X-Community-Read...
Text is not SVG - cannot display \ No newline at end of file From 989a3e30f28be4364f2631c767f05e8dabed06c5 Mon Sep 17 00:00:00 2001 From: elweyn Date: Fri, 31 Mar 2023 07:16:04 +0200 Subject: [PATCH 023/153] Import order fixed by lint --- .../src/emails/sendEmailTranslated.test.ts | 2 +- backend/src/emails/sendEmailVariants.test.ts | 4 ++-- .../federation/validateCommunities.test.ts | 2 +- backend/src/graphql/model/Transaction.ts | 2 +- .../src/graphql/resolver/BalanceResolver.ts | 4 ++-- .../resolver/ContributionLinkResolver.ts | 10 +++++----- .../graphql/resolver/ContributionResolver.ts | 20 +++++++++---------- .../resolver/TransactionLinkResolver.test.ts | 2 +- .../resolver/TransactionLinkResolver.ts | 8 ++++---- .../resolver/TransactionResolver.test.ts | 2 +- .../graphql/resolver/TransactionResolver.ts | 8 ++++---- backend/src/graphql/resolver/UserResolver.ts | 4 ++-- .../graphql/resolver/util/creations.test.ts | 2 +- backend/src/server/LogError.test.ts | 2 +- backend/src/util/validate.ts | 2 +- backend/src/util/virtualTransactions.ts | 2 +- 16 files changed, 38 insertions(+), 38 deletions(-) diff --git a/backend/src/emails/sendEmailTranslated.test.ts b/backend/src/emails/sendEmailTranslated.test.ts index f2fae2746..762b88cf0 100644 --- a/backend/src/emails/sendEmailTranslated.test.ts +++ b/backend/src/emails/sendEmailTranslated.test.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/unbound-method */ import { createTransport } from 'nodemailer' -import { logger, i18n } from '@test/testSetup' import { sendEmailTranslated } from './sendEmailTranslated' +import { logger, i18n } from '@test/testSetup' import CONFIG from '@/config' CONFIG.EMAIL = false diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index 3c8d22f95..399ed89ac 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -4,8 +4,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { Decimal } from 'decimal.js-light' -import { testEnvironment } from '@test/helpers' -import { logger, i18n as localization } from '@test/testSetup' import { sendAddedContributionMessageEmail, sendAccountActivationEmail, @@ -18,6 +16,8 @@ import { sendTransactionReceivedEmail, } from './sendEmailVariants' import { sendEmailTranslated } from './sendEmailTranslated' +import { testEnvironment } from '@test/helpers' +import { logger, i18n as localization } from '@test/testSetup' import CONFIG from '@/config' let con: any diff --git a/backend/src/federation/validateCommunities.test.ts b/backend/src/federation/validateCommunities.test.ts index 57d9bd65f..a99bb3274 100644 --- a/backend/src/federation/validateCommunities.test.ts +++ b/backend/src/federation/validateCommunities.test.ts @@ -6,9 +6,9 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { Community as DbCommunity } from '@entity/Community' +import { validateCommunities } from './validateCommunities' import { logger } from '@test/testSetup' import { testEnvironment, cleanDB } from '@test/helpers' -import { validateCommunities } from './validateCommunities' let con: any let testEnv: any diff --git a/backend/src/graphql/model/Transaction.ts b/backend/src/graphql/model/Transaction.ts index 9e2be85dd..8f0d1eadc 100644 --- a/backend/src/graphql/model/Transaction.ts +++ b/backend/src/graphql/model/Transaction.ts @@ -1,9 +1,9 @@ import { ObjectType, Field, Int } from 'type-graphql' import { Transaction as dbTransaction } from '@entity/Transaction' import { Decimal } from 'decimal.js-light' -import { TransactionTypeId } from '@enum/TransactionTypeId' import { Decay } from './Decay' import { User } from './User' +import { TransactionTypeId } from '@enum/TransactionTypeId' @ObjectType() export class Transaction { diff --git a/backend/src/graphql/resolver/BalanceResolver.ts b/backend/src/graphql/resolver/BalanceResolver.ts index 520b8c094..31e2384d4 100644 --- a/backend/src/graphql/resolver/BalanceResolver.ts +++ b/backend/src/graphql/resolver/BalanceResolver.ts @@ -6,10 +6,10 @@ import { getCustomRepository } from '@dbTools/typeorm' import { Transaction as dbTransaction } from '@entity/Transaction' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' -import { TransactionLinkRepository } from '@repository/TransactionLink' -import { Balance } from '@model/Balance' import { GdtResolver } from './GdtResolver' import { getLastTransaction } from './util/getLastTransaction' +import { TransactionLinkRepository } from '@repository/TransactionLink' +import { Balance } from '@model/Balance' import { backendLogger as logger } from '@/server/logger' import { Context, getUser } from '@/server/context' diff --git a/backend/src/graphql/resolver/ContributionLinkResolver.ts b/backend/src/graphql/resolver/ContributionLinkResolver.ts index dc88bea07..4b19c36e1 100644 --- a/backend/src/graphql/resolver/ContributionLinkResolver.ts +++ b/backend/src/graphql/resolver/ContributionLinkResolver.ts @@ -3,11 +3,6 @@ import { Resolver, Args, Arg, Authorized, Mutation, Query, Int, Ctx } from 'type import { MoreThan, IsNull } from '@dbTools/typeorm' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' -import { ContributionLinkList } from '@model/ContributionLinkList' -import { ContributionLink } from '@model/ContributionLink' -import ContributionLinkArgs from '@arg/ContributionLinkArgs' -import { Order } from '@enum/Order' -import Paginated from '@arg/Paginated' import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' import { isStartEndDateValid } from './util/creations' import { @@ -16,6 +11,11 @@ import { MEMO_MAX_CHARS, MEMO_MIN_CHARS, } from './const/const' +import { ContributionLinkList } from '@model/ContributionLinkList' +import { ContributionLink } from '@model/ContributionLink' +import ContributionLinkArgs from '@arg/ContributionLinkArgs' +import { Order } from '@enum/Order' +import Paginated from '@arg/Paginated' import { RIGHTS } from '@/auth/RIGHTS' // TODO: this is a strange construct diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 6e412fc72..c48b05920 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -9,6 +9,16 @@ import { UserContact } from '@entity/UserContact' import { User as DbUser } from '@entity/User' import { Transaction as DbTransaction } from '@entity/Transaction' +import { + getUserCreation, + validateContribution, + updateCreations, + isValidDateString, + getOpenCreations, +} from './util/creations' +import { findContributions } from './util/findContributions' +import { getLastTransaction } from './util/getLastTransaction' +import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { AdminUpdateContribution } from '@model/AdminUpdateContribution' import { Contribution, ContributionListResult } from '@model/Contribution' import { Decay } from '@model/Decay' @@ -23,16 +33,6 @@ import ContributionArgs from '@arg/ContributionArgs' import Paginated from '@arg/Paginated' import AdminCreateContributionArgs from '@arg/AdminCreateContributionArgs' import AdminUpdateContributionArgs from '@arg/AdminUpdateContributionArgs' -import { - getUserCreation, - validateContribution, - updateCreations, - isValidDateString, - getOpenCreations, -} from './util/creations' -import { findContributions } from './util/findContributions' -import { getLastTransaction } from './util/getLastTransaction' -import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { RIGHTS } from '@/auth/RIGHTS' import { Context, getUser, getClientTimezoneOffset } from '@/server/context' diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts index 96eb99ea7..4f72276d4 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts @@ -13,10 +13,10 @@ import { GraphQLError } from 'graphql' import { Transaction } from '@entity/Transaction' import { Event as DbEvent } from '@entity/Event' import { UserContact } from '@entity/UserContact' +import { transactionLinkCode } from './TransactionLinkResolver' import { cleanDB, testEnvironment, resetToken, resetEntity } from '@test/helpers' import { UnconfirmedContribution } from '@model/UnconfirmedContribution' import { logger } from '@test/testSetup' -import { transactionLinkCode } from './TransactionLinkResolver' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { peterLustig } from '@/seeds/users/peter-lustig' import { creationFactory } from '@/seeds/factory/creation' diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 534db02c1..3ff6fd36a 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -10,6 +10,10 @@ import { Contribution as DbContribution } from '@entity/Contribution' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' import { Resolver, Args, Arg, Authorized, Ctx, Mutation, Query, Int } from 'type-graphql' +import transactionLinkList from './util/transactionLinkList' +import { getLastTransaction } from './util/getLastTransaction' +import { executeTransaction } from './TransactionResolver' +import { getUserCreation, validateContribution } from './util/creations' import { User } from '@model/User' import { ContributionLink } from '@model/ContributionLink' import { Decay } from '@model/Decay' @@ -22,10 +26,6 @@ import TransactionLinkArgs from '@arg/TransactionLinkArgs' import Paginated from '@arg/Paginated' import TransactionLinkFilters from '@arg/TransactionLinkFilters' import QueryLinkResult from '@union/QueryLinkResult' -import transactionLinkList from './util/transactionLinkList' -import { getLastTransaction } from './util/getLastTransaction' -import { executeTransaction } from './TransactionResolver' -import { getUserCreation, validateContribution } from './util/creations' import { backendLogger as logger } from '@/server/logger' import { Context, getUser, getClientTimezoneOffset } from '@/server/context' import { calculateBalance } from '@/util/validate' diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index 17ed81676..6d039784e 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -10,9 +10,9 @@ import { Transaction } from '@entity/Transaction' import { User } from '@entity/User' import { GraphQLError } from 'graphql' import { Event as DbEvent } from '@entity/Event' +import { findUserByEmail } from './UserResolver' import { cleanDB, testEnvironment } from '@test/helpers' import { logger } from '@test/testSetup' -import { findUserByEmail } from './UserResolver' import { EventType } from '@/event/Event' import { userFactory } from '@/seeds/factory/user' import { diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index e0ac5d435..a699e7291 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -9,6 +9,10 @@ import { getCustomRepository, getConnection, In } from '@dbTools/typeorm' import { User as dbUser } from '@entity/User' import { Transaction as dbTransaction } from '@entity/Transaction' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' +import { getLastTransaction } from './util/getLastTransaction' +import { findUserByEmail } from './UserResolver' +import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' +import { BalanceResolver } from './BalanceResolver' import { TransactionRepository } from '@repository/Transaction' import { TransactionLinkRepository } from '@repository/TransactionLink' @@ -19,10 +23,6 @@ import { Order } from '@enum/Order' import { TransactionTypeId } from '@enum/TransactionTypeId' import TransactionSendArgs from '@arg/TransactionSendArgs' import Paginated from '@arg/Paginated' -import { getLastTransaction } from './util/getLastTransaction' -import { findUserByEmail } from './UserResolver' -import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' -import { BalanceResolver } from './BalanceResolver' import { calculateBalance } from '@/util/validate' import { backendLogger as logger } from '@/server/logger' diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index aca72b5d0..0c1cefd12 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -21,6 +21,8 @@ import { User as DbUser } from '@entity/User' import { UserContact as DbUserContact } from '@entity/UserContact' import { TransactionLink as DbTransactionLink } from '@entity/TransactionLink' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' +import { FULL_CREATION_AVAILABLE } from './const/const' +import { getUserCreations } from './util/creations' import { PasswordEncryptionType } from '@enum/PasswordEncryptionType' import { UserRepository } from '@repository/User' @@ -36,8 +38,6 @@ import UnsecureLoginArgs from '@arg/UnsecureLoginArgs' import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs' import Paginated from '@arg/Paginated' import SearchUsersArgs from '@arg/SearchUsersArgs' -import { FULL_CREATION_AVAILABLE } from './const/const' -import { getUserCreations } from './util/creations' import { getTimeDurationObject, printTimeDuration } from '@/util/time' import { sendAccountActivationEmail, diff --git a/backend/src/graphql/resolver/util/creations.test.ts b/backend/src/graphql/resolver/util/creations.test.ts index 5b6b40e2b..7461401c8 100644 --- a/backend/src/graphql/resolver/util/creations.test.ts +++ b/backend/src/graphql/resolver/util/creations.test.ts @@ -6,8 +6,8 @@ import { User } from '@entity/User' import { Contribution } from '@entity/Contribution' -import { testEnvironment, cleanDB, contributionDateFormatter } from '@test/helpers' import { getUserCreation } from './creations' +import { testEnvironment, cleanDB, contributionDateFormatter } from '@test/helpers' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { peterLustig } from '@/seeds/users/peter-lustig' import { userFactory } from '@/seeds/factory/user' diff --git a/backend/src/server/LogError.test.ts b/backend/src/server/LogError.test.ts index b7a067e28..318a477ef 100644 --- a/backend/src/server/LogError.test.ts +++ b/backend/src/server/LogError.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/unbound-method */ -import { logger } from '@test/testSetup' import LogError from './LogError' +import { logger } from '@test/testSetup' describe('LogError', () => { it('logs an Error when created', () => { diff --git a/backend/src/util/validate.ts b/backend/src/util/validate.ts index b70c90344..aaadbdd31 100644 --- a/backend/src/util/validate.ts +++ b/backend/src/util/validate.ts @@ -1,9 +1,9 @@ import { Decimal } from 'decimal.js-light' import { getCustomRepository } from '@dbTools/typeorm' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' +import { calculateDecay } from './decay' import { TransactionLinkRepository } from '@repository/TransactionLink' import { Decay } from '@model/Decay' -import { calculateDecay } from './decay' import { getLastTransaction } from '@/graphql/resolver/util/getLastTransaction' function isStringBoolean(value: string): boolean { diff --git a/backend/src/util/virtualTransactions.ts b/backend/src/util/virtualTransactions.ts index aea4ebb66..7810ad871 100644 --- a/backend/src/util/virtualTransactions.ts +++ b/backend/src/util/virtualTransactions.ts @@ -2,10 +2,10 @@ import { SaveOptions, RemoveOptions } from '@dbTools/typeorm' import { Transaction as dbTransaction } from '@entity/Transaction' import { Decimal } from 'decimal.js-light' +import { calculateDecay } from './decay' import { Transaction } from '@model/Transaction' import { TransactionTypeId } from '@enum/TransactionTypeId' import { User } from '@model/User' -import { calculateDecay } from './decay' const defaultModelFunctions = { hasId: function (): boolean { From 71266dcc2d3675f2fe6bac12d4a7c466e6493887 Mon Sep 17 00:00:00 2001 From: elweyn Date: Fri, 31 Mar 2023 07:16:21 +0200 Subject: [PATCH 024/153] Fix linting rules --- .../graphql/resolver/KlicktippResolver.test.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/backend/src/graphql/resolver/KlicktippResolver.test.ts b/backend/src/graphql/resolver/KlicktippResolver.test.ts index 0e64a779f..d7a7e8255 100644 --- a/backend/src/graphql/resolver/KlicktippResolver.test.ts +++ b/backend/src/graphql/resolver/KlicktippResolver.test.ts @@ -1,19 +1,22 @@ -import { cleanDB, resetToken, testEnvironment } from '@test/helpers' -import { logger, i18n as localization } from '@test/testSetup' +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { GraphQLError } from 'graphql' import { UserContact } from '@entity/UserContact' import { Event as DbEvent } from '@entity/Event' +import { logger, i18n as localization } from '@test/testSetup' +import { cleanDB, resetToken, testEnvironment } from '@test/helpers' import { login, subscribeNewsletter, unsubscribeNewsletter } from '@/seeds/graphql/mutations' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { userFactory } from '@/seeds/factory/user' import { EventType } from '@/event/Event' -let testEnv: any, mutate: any, query: any, con: any +let testEnv: any, mutate: any, con: any beforeAll(async () => { testEnv = await testEnvironment(logger, localization) mutate = testEnv.mutate - query = testEnv.query con = testEnv.con await cleanDB() }) @@ -24,10 +27,8 @@ afterAll(async () => { }) describe('KlicktippResolver', () => { - let bibi - beforeAll(async () => { - bibi = await userFactory(testEnv, bibiBloxberg) + await userFactory(testEnv, bibiBloxberg) }) afterAll(async () => { From d326f00c0bee44d5306ccbfd0677a3da05d78346 Mon Sep 17 00:00:00 2001 From: elweyn Date: Fri, 31 Mar 2023 07:28:52 +0200 Subject: [PATCH 025/153] Fix linting --- backend/src/event/EVENT_SUBSCRIBE_NEWSLETTER.ts | 3 ++- backend/src/event/EVENT_UNSUBSCRIBE_NEWSLETTER.ts | 3 ++- .../src/graphql/resolver/CommunityResolver.test.ts | 1 + .../src/graphql/resolver/KlicktippResolver.test.ts | 12 +++++++----- backend/src/graphql/resolver/KlicktippResolver.ts | 2 +- backend/src/middleware/klicktippMiddleware.ts | 3 +-- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/backend/src/event/EVENT_SUBSCRIBE_NEWSLETTER.ts b/backend/src/event/EVENT_SUBSCRIBE_NEWSLETTER.ts index f4207d059..a9656b6fa 100644 --- a/backend/src/event/EVENT_SUBSCRIBE_NEWSLETTER.ts +++ b/backend/src/event/EVENT_SUBSCRIBE_NEWSLETTER.ts @@ -1,5 +1,6 @@ -import { User as DbUser } from '@entity/User' import { Event as DbEvent } from '@entity/Event' +import { User as DbUser } from '@entity/User' + import { Event, EventType } from './Event' export const EVENT_SUBSCRIBE_NEWSLETTER = async (user: DbUser): Promise => diff --git a/backend/src/event/EVENT_UNSUBSCRIBE_NEWSLETTER.ts b/backend/src/event/EVENT_UNSUBSCRIBE_NEWSLETTER.ts index 6a6946e3f..1b2561894 100644 --- a/backend/src/event/EVENT_UNSUBSCRIBE_NEWSLETTER.ts +++ b/backend/src/event/EVENT_UNSUBSCRIBE_NEWSLETTER.ts @@ -1,5 +1,6 @@ -import { User as DbUser } from '@entity/User' import { Event as DbEvent } from '@entity/Event' +import { User as DbUser } from '@entity/User' + import { Event, EventType } from './Event' export const EVENT_UNSUBSCRIBE_NEWSLETTER = async (user: DbUser): Promise => diff --git a/backend/src/graphql/resolver/CommunityResolver.test.ts b/backend/src/graphql/resolver/CommunityResolver.test.ts index 2cb27de31..5b4b26cad 100644 --- a/backend/src/graphql/resolver/CommunityResolver.test.ts +++ b/backend/src/graphql/resolver/CommunityResolver.test.ts @@ -8,6 +8,7 @@ import { Community as DbCommunity } from '@entity/Community' import { testEnvironment } from '@test/helpers' + import { getCommunities } from '@/seeds/graphql/queries' let query: any diff --git a/backend/src/graphql/resolver/KlicktippResolver.test.ts b/backend/src/graphql/resolver/KlicktippResolver.test.ts index d7a7e8255..02b3e0492 100644 --- a/backend/src/graphql/resolver/KlicktippResolver.test.ts +++ b/backend/src/graphql/resolver/KlicktippResolver.test.ts @@ -2,15 +2,17 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import { GraphQLError } from 'graphql' -import { UserContact } from '@entity/UserContact' import { Event as DbEvent } from '@entity/Event' -import { logger, i18n as localization } from '@test/testSetup' +import { UserContact } from '@entity/UserContact' +import { GraphQLError } from 'graphql' + import { cleanDB, resetToken, testEnvironment } from '@test/helpers' +import { logger, i18n as localization } from '@test/testSetup' + +import { EventType } from '@/event/Event' +import { userFactory } from '@/seeds/factory/user' import { login, subscribeNewsletter, unsubscribeNewsletter } from '@/seeds/graphql/mutations' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' -import { userFactory } from '@/seeds/factory/user' -import { EventType } from '@/event/Event' let testEnv: any, mutate: any, con: any diff --git a/backend/src/graphql/resolver/KlicktippResolver.ts b/backend/src/graphql/resolver/KlicktippResolver.ts index e03e81440..03b591c4d 100644 --- a/backend/src/graphql/resolver/KlicktippResolver.ts +++ b/backend/src/graphql/resolver/KlicktippResolver.ts @@ -1,8 +1,8 @@ import { Resolver, Authorized, Mutation, Ctx } from 'type-graphql' import { unsubscribe, klicktippSignIn } from '@/apis/KlicktippController' -import { EVENT_UNSUBSCRIBE_NEWSLETTER, EVENT_SUBSCRIBE_NEWSLETTER } from '@/event/Event' import { RIGHTS } from '@/auth/RIGHTS' +import { EVENT_UNSUBSCRIBE_NEWSLETTER, EVENT_SUBSCRIBE_NEWSLETTER } from '@/event/Event' import { Context, getUser } from '@/server/context' @Resolver() diff --git a/backend/src/middleware/klicktippMiddleware.ts b/backend/src/middleware/klicktippMiddleware.ts index 82391a614..cc0335c09 100644 --- a/backend/src/middleware/klicktippMiddleware.ts +++ b/backend/src/middleware/klicktippMiddleware.ts @@ -6,8 +6,7 @@ import { MiddlewareFn } from 'type-graphql' import { KlickTipp } from '@model/KlickTipp' -import { /* klicktippSignIn, */ getKlickTippUser } from '@/apis/KlicktippController' -import CONFIG from '@/config' +import { getKlickTippUser } from '@/apis/KlicktippController' import { klickTippLogger as logger } from '@/server/logger' // export const klicktippRegistrationMiddleware: MiddlewareFn = async ( From dc9efdae6713f14ae4d91d657c8babf40cc00e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 31 Mar 2023 20:12:59 +0200 Subject: [PATCH 026/153] first migration step --- ...-community-sendcoins-transactions_table.ts | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 database/migrations/0065-x-community-sendcoins-transactions_table.ts diff --git a/database/migrations/0065-x-community-sendcoins-transactions_table.ts b/database/migrations/0065-x-community-sendcoins-transactions_table.ts new file mode 100644 index 000000000..73b854d70 --- /dev/null +++ b/database/migrations/0065-x-community-sendcoins-transactions_table.ts @@ -0,0 +1,134 @@ +/* MIGRATION TO add users that have a transaction but do not exist */ + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn( + 'ALTER TABLE `transactions` MODIFY COLUMN `previous` int(10) unsigned DEFAULT NULL NULL AFTER `id`;', + ) + await queryFn( + 'ALTER TABLE `transactions` MODIFY COLUMN `type_id` int(10) DEFAULT NULL NULL AFTER `previous`;', + ) + await queryFn( + 'ALTER TABLE `transactions` MODIFY COLUMN `transaction_link_id` int(10) unsigned DEFAULT NULL NULL AFTER `type_id`;', + ) + await queryFn( + 'ALTER TABLE `transactions` MODIFY COLUMN `amount` decimal(40,20) DEFAULT NULL NULL AFTER `transaction_link_id`;', + ) + await queryFn( + 'ALTER TABLE `transactions` MODIFY COLUMN `balance` decimal(40,20) DEFAULT NULL NULL AFTER `amount`;', + ) + await queryFn( + 'ALTER TABLE `transactions` MODIFY COLUMN `balance_date` datetime(3) DEFAULT current_timestamp(3) NOT NULL AFTER `balance`;', + ) + await queryFn( + 'ALTER TABLE `transactions` MODIFY COLUMN `decay` decimal(40,20) DEFAULT NULL NULL AFTER `balance_date`;', + ) + await queryFn( + 'ALTER TABLE `transactions` MODIFY COLUMN `decay_start` datetime(3) DEFAULT NULL NULL AFTER `decay`;', + ) + await queryFn( + 'ALTER TABLE `transactions` MODIFY COLUMN `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL AFTER `decay_start`;', + ) + await queryFn( + 'ALTER TABLE `transactions` MODIFY COLUMN `creation_date` datetime(3) DEFAULT NULL NULL AFTER `memo`;', + ) + await queryFn( + 'ALTER TABLE `transactions` MODIFY COLUMN `user_id` int(10) unsigned NOT NULL AFTER `creation_date`;', + ) + await queryFn( + 'ALTER TABLE `transactions` ADD COLUMN `user_gradido_id` char(36) DEFAULT NULL NULL AFTER `user_id`;', + ) + await queryFn( + 'ALTER TABLE `transactions` ADD COLUMN `user_community_uuid` char(36) DEFAULT NULL NULL AFTER `user_gradido_id`;', + ) + await queryFn( + 'ALTER TABLE `transactions` ADD COLUMN `user_name` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL NULL AFTER `user_community_uuid`;', + ) + await queryFn( + 'ALTER TABLE `transactions` MODIFY COLUMN `linked_user_id` int(10) unsigned DEFAULT NULL NULL AFTER `user_name`;', + ) + await queryFn( + 'ALTER TABLE `transactions` ADD COLUMN `linked_user_gradido_id` char(36) DEFAULT NULL NULL AFTER `linked_user_id`;', + ) + await queryFn( + 'ALTER TABLE `transactions` ADD COLUMN `linked_user_community_uuid` char(36) DEFAULT NULL NULL AFTER `linked_user_gradido_id`;', + ) + await queryFn( + 'ALTER TABLE `transactions` ADD COLUMN `linked_user_name` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL NULL AFTER `linked_user_community_uuid`;', + ) + await queryFn( + 'ALTER TABLE `transactions` MODIFY COLUMN `linked_transaction_id` int(10) DEFAULT NULL NULL AFTER `linked_user_name`;', + ) + + await queryFn(` + CREATE TABLE IF NOT EXISTS \`pending_transactions\` ( + \`id\` int(10) unsigned NOT NULL AUTO_INCREMENT, + \`previous\` int(10) unsigned DEFAULT NULL NULL, + \`type_id\` int(10) DEFAULT NULL NULL, + \`transaction_link_id\` int(10) unsigned DEFAULT NULL NULL, + \`amount\` decimal(40,20) DEFAULT NULL NULL, + \`balance\` decimal(40,20) DEFAULT NULL NULL, + \`balance_date\` datetime(3) DEFAULT current_timestamp(3) NOT NULL, + \`decay\` decimal(40,20) DEFAULT NULL NULL, + \`decay_start\` datetime(3) DEFAULT NULL NULL, + \`memo\` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + \`creation_date\` datetime(3) DEFAULT NULL NULL, + \`user_id\` int(10) unsigned NOT NULL, + \`user_gradido_id\` char(36) NOT NULL, + \`user_community_uuid\` char(36) NOT NULL, + \`user_name\` varchar(512) COLLATE utf8mb4_unicode_ci NOT NULL, + \`linked_user_id\` int(10) unsigned DEFAULT NULL NULL, + \`linked_user_gradido_id\` char(36) NOT NULL, + \`linked_user_community_uuid\` char(36) NOT NULL, + \`linked_user_name\` varchar(512) NULL, + \`linked_transaction_id\` int(10) DEFAULT NULL NULL, + \`x_transaction_state\` varchar(100) NOT NULL COMMENT 'States to handle 2-Phase-Commit handshake', + \`created_at\` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + \`updated_at\` datetime(3) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(3), + \`deleted_at\` datetime(3) NULL DEFAULT NULL, + PRIMARY KEY (\`id\`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`) + + /* + const userIds = await queryFn(` + SELECT user_id FROM transactions + WHERE NOT EXISTS (SELECT id FROM users WHERE id = user_id) GROUP BY user_id;`) + + for (let i = 0; i < missingUserIds.length; i++) { + let gradidoId = '' + let countIds: any[] = [] + do { + gradidoId = uuidv4() + countIds = await queryFn( + `SELECT COUNT(*) FROM \`users\` WHERE \`gradido_id\` = "${gradidoId}"`, + ) + } while (countIds[0] > 0) + + const userContact = (await queryFn(` + INSERT INTO user_contacts + (type, user_id, email, email_checked, created_at, deleted_at) + VALUES + ('EMAIL', ${missingUserIds[i].user_id}, 'deleted.user${missingUserIds[i].user_id}@gradido.net', 0, NOW(), NOW());`)) as unknown as OkPacket + + await queryFn(` + INSERT INTO users + (id, gradido_id, email_id, first_name, last_name, deleted_at, password_encryption_type, created_at, language) + VALUES + (${missingUserIds[i].user_id}, '${gradidoId}', ${userContact.insertId}, 'DELETED', 'USER', NOW(), 0, NOW(), 'de');`) + } + */ +} + +/* 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_community_uuid`;') + await queryFn('ALTER TABLE `transactions` DROP COLUMN `user_name`;') + await queryFn('ALTER TABLE `transactions` DROP COLUMN `linked_user_gradido_id`;') + await queryFn('ALTER TABLE `transactions` DROP COLUMN `linked_user_community_uuid`;') + await queryFn('ALTER TABLE `transactions` DROP COLUMN `linked_user_name`;') + await queryFn(`DROP TABLE IF EXISTS pending_transactions;`) +} From 51d572f0b795efa54f771a6d19db8675a1efa5cd Mon Sep 17 00:00:00 2001 From: elweyn Date: Mon, 3 Apr 2023 14:58:26 +0200 Subject: [PATCH 027/153] Implement callKlickTippAPI --- backend/src/apis/KlicktippController.ts | 178 ++++++++++++++---------- 1 file changed, 103 insertions(+), 75 deletions(-) diff --git a/backend/src/apis/KlicktippController.ts b/backend/src/apis/KlicktippController.ts index 1278b0d9c..d414bb9ec 100644 --- a/backend/src/apis/KlicktippController.ts +++ b/backend/src/apis/KlicktippController.ts @@ -4,107 +4,135 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ import CONFIG from '@/config' +import LogError from '@/server/LogError' // eslint-disable-next-line import/no-relative-parent-imports import KlicktippConnector from 'klicktipp-api' const klicktippConnector = new KlicktippConnector() -export const klicktippSignIn = async ( +const callKlickTippAPI = (callback: (arg0: any) => any, args: any) => { + if (!CONFIG.KLICKTIPP) { + return true + } + return callback(args) +} + +export const klicktippSignIn = ( email: string, language: string, firstName?: string, lastName?: string, ): Promise => { - if (!CONFIG.KLICKTIPP) { - return true - } - const fields = { - fieldFirstName: firstName, - fieldLastName: lastName, - } - const apiKey = language === 'de' ? CONFIG.KLICKTIPP_APIKEY_DE : CONFIG.KLICKTIPP_APIKEY_EN - const result = await klicktippConnector.signin(apiKey, email, fields) - return result + return callKlickTippAPI( + ({ fieldFirstName, fieldLastName, language, email }) => { + const fields = { + fieldFirstName, + fieldLastName, + } + const apiKey = language === 'de' ? CONFIG.KLICKTIPP_APIKEY_DE : CONFIG.KLICKTIPP_APIKEY_EN + return klicktippConnector.signin(apiKey, email, fields) + }, + { + fieldFirstName: firstName, + fieldLastName: lastName, + language, + email, + }, + ) } -export const signout = async (email: string, language: string): Promise => { - if (!CONFIG.KLICKTIPP) { - return true - } - const apiKey = language === 'de' ? CONFIG.KLICKTIPP_APIKEY_DE : CONFIG.KLICKTIPP_APIKEY_EN - const result = await klicktippConnector.signoff(apiKey, email) - return result +export const signout = (email: string, language: string): Promise => { + return callKlickTippAPI( + async ({ language, email }) => { + const apiKey = language === 'de' ? CONFIG.KLICKTIPP_APIKEY_DE : CONFIG.KLICKTIPP_APIKEY_EN + const result = await klicktippConnector.signoff(apiKey, email) + return result + }, + { + email, + language, + }, + ) } -export const unsubscribe = async (email: string): Promise => { - if (!CONFIG.KLICKTIPP) { - return true - } - const isLogin = await loginKlicktippUser() - if (isLogin) { - return await klicktippConnector.unsubscribe(email) - } - throw new Error(`Could not unsubscribe ${email}`) +export const unsubscribe = (email: string): Promise => { + return callKlickTippAPI( + async ({ email }) => { + const isLogin = await loginKlicktippUser() + if (isLogin) { + return klicktippConnector.unsubscribe(email) + } + throw new LogError(`Could not unsubscribe ${email}`) + }, + { + email, + }, + ) } -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 - } - return false +export const getKlickTippUser = (email: string): Promise => { + return callKlickTippAPI( + async ({ email }) => { + const isLogin = await loginKlicktippUser() + if (isLogin) { + const subscriberId = await klicktippConnector.subscriberSearch(email) + return klicktippConnector.subscriberGet(subscriberId) + } + throw new LogError(`Could not get subscriber ${email}`) + }, + { + email, + }, + ) } -export const loginKlicktippUser = async (): Promise => { - if (!CONFIG.KLICKTIPP) { - return true - } - return await klicktippConnector.login(CONFIG.KLICKTIPP_USER, CONFIG.KLICKTIPP_PASSWORD) +export const loginKlicktippUser = (): Promise => { + return callKlickTippAPI(() => { + return klicktippConnector.login(CONFIG.KLICKTIPP_USER, CONFIG.KLICKTIPP_PASSWORD) + }, {}) } -export const logoutKlicktippUser = async (): Promise => { - if (!CONFIG.KLICKTIPP) { - return true - } - return await klicktippConnector.logout() +export const logoutKlicktippUser = (): Promise => { + return callKlickTippAPI(() => { + return klicktippConnector.logout() + }, {}) } -export const untagUser = async (email: string, tagId: string): Promise => { - if (!CONFIG.KLICKTIPP) { - return true - } - const isLogin = await loginKlicktippUser() - if (isLogin) { - return await klicktippConnector.untag(email, tagId) - } - return false +export const untagUser = (email: string, tagId: string): Promise => { + return callKlickTippAPI( + async ({ email, tagId }) => { + const isLogin = await loginKlicktippUser() + if (isLogin) { + return await klicktippConnector.untag(email, tagId) + } + throw new LogError(`Could not untag ${email}`) + }, + { email, tagId }, + ) } -export const tagUser = async (email: string, tagIds: string): Promise => { - if (!CONFIG.KLICKTIPP) { - return true - } - const isLogin = await loginKlicktippUser() - if (isLogin) { - return await klicktippConnector.tag(email, tagIds) - } - return false +export const tagUser = (email: string, tagIds: string): Promise => { + return callKlickTippAPI( + async ({ email, tagIds }) => { + const isLogin = await loginKlicktippUser() + if (isLogin) { + return klicktippConnector.tag(email, tagIds) + } + throw new LogError(`Could not tag ${email}`) + }, + { email, tagIds }, + ) } -export const getKlicktippTagMap = async () => { - if (!CONFIG.KLICKTIPP) { - return '' - } - const isLogin = await loginKlicktippUser() - if (isLogin) { - return await klicktippConnector.tagIndex() - } - return '' +export const getKlicktippTagMap = (): Promise => { + return callKlickTippAPI(async () => { + const isLogin = await loginKlicktippUser() + if (isLogin) { + return klicktippConnector.tagIndex() + } + throw new LogError(`Could not get tagIndexes`) + }, {}) } From 588afdb8d7e122930f0f5f67b6b8a30d76d2af70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 4 Apr 2023 01:56:34 +0200 Subject: [PATCH 028/153] add new attributes for x-community-sendCoins --- backend/src/config/index.ts | 2 +- .../graphql/resolver/ContributionResolver.ts | 2 + .../graphql/resolver/TransactionResolver.ts | 8 + backend/src/util/virtualTransactions.ts | 70 +++++---- .../Transaction.ts | 147 ++++++++++++++++++ database/entity/Transaction.ts | 2 +- ...-community-sendcoins-transactions_table.ts | 42 ++--- 7 files changed, 214 insertions(+), 59 deletions(-) create mode 100644 database/entity/0065-x-community-sendcoins-transactions_table/Transaction.ts diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 23ede1f27..959700814 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -12,7 +12,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0064-event_rename', + DB_VERSION: '0065-x-community-sendcoins-transactions_table', 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 diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 5969eaef2..7068fd4ab 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -501,6 +501,8 @@ export class ContributionResolver { transaction.typeId = TransactionTypeId.CREATION transaction.memo = contribution.memo transaction.userId = contribution.userId + transaction.userGradidoID = user.gradidoID + transaction.userName = user.firstName + ' ' + user.lastName transaction.previous = lastTransaction ? lastTransaction.id : null transaction.amount = contribution.amount transaction.creationDate = contribution.contributionDate diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 430cdb363..4706df794 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -87,7 +87,11 @@ export const executeTransaction = async ( transactionSend.typeId = TransactionTypeId.SEND transactionSend.memo = memo transactionSend.userId = sender.id + transactionSend.userGradidoID = sender.gradidoID + transactionSend.userName = sender.firstName + ' ' + sender.lastName transactionSend.linkedUserId = recipient.id + transactionSend.linkedUserGradidoID = recipient.gradidoID + transactionSend.linkedUserName = recipient.firstName + ' ' + recipient.lastName transactionSend.amount = amount.mul(-1) transactionSend.balance = sendBalance.balance transactionSend.balanceDate = receivedCallDate @@ -103,7 +107,11 @@ export const executeTransaction = async ( transactionReceive.typeId = TransactionTypeId.RECEIVE transactionReceive.memo = memo transactionReceive.userId = recipient.id + transactionReceive.userGradidoID = recipient.gradidoID + transactionReceive.userName = recipient.firstName + ' ' + recipient.lastName transactionReceive.linkedUserId = sender.id + transactionReceive.linkedUserGradidoID = sender.gradidoID + transactionReceive.linkedUserName = sender.firstName + ' ' + sender.lastName transactionReceive.amount = amount const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate) transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount diff --git a/backend/src/util/virtualTransactions.ts b/backend/src/util/virtualTransactions.ts index 68a37746b..cd73f9feb 100644 --- a/backend/src/util/virtualTransactions.ts +++ b/backend/src/util/virtualTransactions.ts @@ -40,19 +40,25 @@ const virtualLinkTransaction = ( user: User, ): Transaction => { const linkDbTransaction: dbTransaction = { - id: -2, - userId: -1, - previous: -1, - typeId: TransactionTypeId.LINK_SUMMARY, - amount: amount.toDecimalPlaces(2, Decimal.ROUND_FLOOR), - balance: balance.toDecimalPlaces(2, Decimal.ROUND_DOWN), - balanceDate: validUntil, - decayStart: createdAt, - decay: decay.toDecimalPlaces(2, Decimal.ROUND_FLOOR), - memo: '', - creationDate: null, - contribution: null, - ...defaultModelFunctions, + id: -2, + userId: -1, + previous: -1, + typeId: TransactionTypeId.LINK_SUMMARY, + amount: amount.toDecimalPlaces(2, Decimal.ROUND_FLOOR), + balance: balance.toDecimalPlaces(2, Decimal.ROUND_DOWN), + balanceDate: validUntil, + decayStart: createdAt, + decay: decay.toDecimalPlaces(2, Decimal.ROUND_FLOOR), + memo: '', + creationDate: null, + contribution: null, + ...defaultModelFunctions, + userGradidoID: '', + userCommunityUuid: '', + userName: '', + linkedUserGradidoID: '', + linkedUserCommunityUuid: '', + linkedUserName: '' } return new Transaction(linkDbTransaction, user) } @@ -67,22 +73,28 @@ const virtualDecayTransaction = ( const decay = calculateDecay(balance, balanceDate, time) // const balance = decay.balance.minus(lastTransaction.balance) const decayDbTransaction: dbTransaction = { - id: -1, - userId: -1, - previous: -1, - typeId: TransactionTypeId.DECAY, - amount: decay.decay ? decay.roundedDecay : new Decimal(0), - balance: decay.balance - .toDecimalPlaces(2, Decimal.ROUND_DOWN) - .minus(holdAvailabeAmount.toString()) - .toDecimalPlaces(2, Decimal.ROUND_DOWN), - balanceDate: time, - decay: decay.decay ? decay.roundedDecay : new Decimal(0), - decayStart: decay.start, - memo: '', - creationDate: null, - contribution: null, - ...defaultModelFunctions, + id: -1, + userId: -1, + previous: -1, + typeId: TransactionTypeId.DECAY, + amount: decay.decay ? decay.roundedDecay : new Decimal(0), + balance: decay.balance + .toDecimalPlaces(2, Decimal.ROUND_DOWN) + .minus(holdAvailabeAmount.toString()) + .toDecimalPlaces(2, Decimal.ROUND_DOWN), + balanceDate: time, + decay: decay.decay ? decay.roundedDecay : new Decimal(0), + decayStart: decay.start, + memo: '', + creationDate: null, + contribution: null, + ...defaultModelFunctions, + userGradidoID: '', + userCommunityUuid: '', + userName: '', + linkedUserGradidoID: '', + linkedUserCommunityUuid: '', + linkedUserName: '' } return new Transaction(decayDbTransaction, user) } diff --git a/database/entity/0065-x-community-sendcoins-transactions_table/Transaction.ts b/database/entity/0065-x-community-sendcoins-transactions_table/Transaction.ts new file mode 100644 index 000000000..b3f88f0d1 --- /dev/null +++ b/database/entity/0065-x-community-sendcoins-transactions_table/Transaction.ts @@ -0,0 +1,147 @@ +import Decimal from 'decimal.js-light' +import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm' +import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' +import { Contribution } from '../Contribution' + +@Entity('transactions') +export class Transaction extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ type: 'int', unsigned: true, unique: true, nullable: true, default: null }) + previous: number | null + + @Column({ name: 'type_id', unsigned: true, nullable: false }) + typeId: number + + @Column({ + name: 'transaction_link_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + transactionLinkId?: number | null + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: false, + transformer: DecimalTransformer, + }) + amount: Decimal + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: false, + transformer: DecimalTransformer, + }) + balance: Decimal + + @Column({ + name: 'balance_date', + type: 'datetime', + default: () => 'CURRENT_TIMESTAMP', + nullable: false, + }) + balanceDate: Date + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: false, + transformer: DecimalTransformer, + }) + decay: Decimal + + @Column({ + name: 'decay_start', + type: 'datetime', + nullable: true, + default: null, + }) + decayStart: Date | null + + @Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' }) + memo: string + + @Column({ name: 'creation_date', type: 'datetime', nullable: true, default: null }) + creationDate: Date | null + + @Column({ name: 'user_id', unsigned: true, nullable: false }) + userId: number + + @Column({ + name: 'user_gradido_id', + length: 36, + nullable: false, + collation: 'utf8mb4_unicode_ci', + }) + userGradidoID: string + + @Column({ + name: 'user_community_uuid', + length: 36, + nullable: false, + collation: 'utf8mb4_unicode_ci', + }) + userCommunityUuid: string + + @Column({ + name: 'user_name', + length: 512, + nullable: false, + collation: 'utf8mb4_unicode_ci', + }) + userName: string + + @Column({ + name: 'linked_user_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + linkedUserId?: number | null + + @Column({ + name: 'linked_user_gradido_id', + length: 36, + nullable: false, + collation: 'utf8mb4_unicode_ci', + }) + linkedUserGradidoID: string + + @Column({ + name: 'linked_user_community_uuid', + length: 36, + nullable: false, + collation: 'utf8mb4_unicode_ci', + }) + linkedUserCommunityUuid: string + + @Column({ + name: 'linked_user_name', + length: 512, + nullable: false, + collation: 'utf8mb4_unicode_ci', + }) + linkedUserName: string + + @Column({ + name: 'linked_transaction_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + linkedTransactionId?: number | null + + @OneToOne(() => Contribution, (contribution) => contribution.transaction) + @JoinColumn({ name: 'id', referencedColumnName: 'transactionId' }) + contribution?: Contribution | null +} diff --git a/database/entity/Transaction.ts b/database/entity/Transaction.ts index 5365b0f70..3e3355b13 100644 --- a/database/entity/Transaction.ts +++ b/database/entity/Transaction.ts @@ -1 +1 @@ -export { Transaction } from './0036-unique_previous_in_transactions/Transaction' +export { Transaction } from './0065-x-community-sendcoins-transactions_table/Transaction' diff --git a/database/migrations/0065-x-community-sendcoins-transactions_table.ts b/database/migrations/0065-x-community-sendcoins-transactions_table.ts index 73b854d70..d3228b8d9 100644 --- a/database/migrations/0065-x-community-sendcoins-transactions_table.ts +++ b/database/migrations/0065-x-community-sendcoins-transactions_table.ts @@ -62,6 +62,20 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis 'ALTER TABLE `transactions` MODIFY COLUMN `linked_transaction_id` int(10) DEFAULT NULL NULL AFTER `linked_user_name`;', ) + await queryFn( + `UPDATE transactions t, users u SET t.user_gradido_id = u.gradido_id, t.user_name = concat(u.first_name, ' ', u.last_name) WHERE t.user_id = u.id and t.user_gradido_id is null;`, + ) + await queryFn( + 'ALTER TABLE `transactions` MODIFY COLUMN `user_gradido_id` char(36) NOT NULL AFTER `user_id`;', + ) + await queryFn( + 'ALTER TABLE `transactions` MODIFY COLUMN `user_name` varchar(512) COLLATE utf8mb4_unicode_ci NOT NULL AFTER `user_community_uuid`;', + ) + + await queryFn( + `UPDATE transactions t, users u SET t.linked_user_gradido_id = u.gradido_id, t.linked_user_name = concat(u.first_name, ' ', u.last_name) WHERE t.linked_user_id = u.id and t.linked_user_id is null and t.linked_user_gradido_id is null;`, + ) + await queryFn(` CREATE TABLE IF NOT EXISTS \`pending_transactions\` ( \`id\` int(10) unsigned NOT NULL AUTO_INCREMENT, @@ -91,34 +105,6 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis PRIMARY KEY (\`id\`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`) - /* - const userIds = await queryFn(` - SELECT user_id FROM transactions - WHERE NOT EXISTS (SELECT id FROM users WHERE id = user_id) GROUP BY user_id;`) - - for (let i = 0; i < missingUserIds.length; i++) { - let gradidoId = '' - let countIds: any[] = [] - do { - gradidoId = uuidv4() - countIds = await queryFn( - `SELECT COUNT(*) FROM \`users\` WHERE \`gradido_id\` = "${gradidoId}"`, - ) - } while (countIds[0] > 0) - - const userContact = (await queryFn(` - INSERT INTO user_contacts - (type, user_id, email, email_checked, created_at, deleted_at) - VALUES - ('EMAIL', ${missingUserIds[i].user_id}, 'deleted.user${missingUserIds[i].user_id}@gradido.net', 0, NOW(), NOW());`)) as unknown as OkPacket - - await queryFn(` - INSERT INTO users - (id, gradido_id, email_id, first_name, last_name, deleted_at, password_encryption_type, created_at, language) - VALUES - (${missingUserIds[i].user_id}, '${gradidoId}', ${userContact.insertId}, 'DELETED', 'USER', NOW(), 0, NOW(), 'de');`) - } - */ } /* eslint-disable @typescript-eslint/no-empty-function */ From a900c163e3a62d2028fed9825f5d5d77402ccd1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 4 Apr 2023 01:58:29 +0200 Subject: [PATCH 029/153] update cypress --- e2e-tests/yarn.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/e2e-tests/yarn.lock b/e2e-tests/yarn.lock index c0f623e47..20956c5d0 100644 --- a/e2e-tests/yarn.lock +++ b/e2e-tests/yarn.lock @@ -2193,10 +2193,10 @@ crypto-browserify@^3.0.0: randombytes "^2.0.0" randomfill "^1.0.3" -cypress@^10.4.0: - version "10.8.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-10.8.0.tgz#12a681f2642b6f13d636bab65d5b71abdb1497a5" - integrity sha512-QVse0dnLm018hgti2enKMVZR9qbIO488YGX06nH5j3Dg1isL38DwrBtyrax02CANU6y8F4EJUuyW6HJKw1jsFA== +cypress@^12.7.0: + version "12.9.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.9.0.tgz#e6ab43cf329fd7c821ef7645517649d72ccf0a12" + integrity sha512-Ofe09LbHKgSqX89Iy1xen2WvpgbvNxDzsWx3mgU1mfILouELeXYGwIib3ItCwoRrRifoQwcBFmY54Vs0zw7QCg== dependencies: "@cypress/request" "^2.88.10" "@cypress/xvfb" "^1.2.4" @@ -2215,7 +2215,7 @@ cypress@^10.4.0: commander "^5.1.0" common-tags "^1.8.0" dayjs "^1.10.4" - debug "^4.3.2" + debug "^4.3.4" enquirer "^2.3.6" eventemitter2 "6.4.7" execa "4.1.0" From c9631f0028dfa38daa063b377a8bf02dc7c86870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 4 Apr 2023 02:20:59 +0200 Subject: [PATCH 030/153] init new tx-attributes --- backend/src/graphql/resolver/TransactionLinkResolver.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index a5f8c0ee6..d10a81c51 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -266,6 +266,8 @@ export class TransactionLinkResolver { transaction.typeId = TransactionTypeId.CREATION transaction.memo = contribution.memo transaction.userId = contribution.userId + transaction.userGradidoID = user.gradidoID + transaction.userName = user.firstName + ' ' + user.lastName transaction.previous = lastTransaction ? lastTransaction.id : null transaction.amount = contribution.amount transaction.creationDate = contribution.contributionDate From 20a80d56a68e31a8b16ce076392a74b014ab6d01 Mon Sep 17 00:00:00 2001 From: elweyn Date: Tue, 4 Apr 2023 09:09:37 +0200 Subject: [PATCH 031/153] Add callCklickTippAPI to addFieldsToSubscriber --- backend/src/apis/KlicktippController.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/backend/src/apis/KlicktippController.ts b/backend/src/apis/KlicktippController.ts index d94b4e132..91c92c855 100644 --- a/backend/src/apis/KlicktippController.ts +++ b/backend/src/apis/KlicktippController.ts @@ -143,9 +143,15 @@ export const addFieldsToSubscriber = async ( newemail = '', newsmsnumber = '', ) => { - const isLogin = await loginKlicktippUser() - if (isLogin) { - const subscriberId = await klicktippConnector.subscriberSearch(email) - return await klicktippConnector.subscriberUpdate(subscriberId, fields, newemail, newsmsnumber) - } + return callKlickTippAPI( + async ({ email, fields, newemail, newsmsnumber }) => { + const isLogin = await loginKlicktippUser() + if (isLogin) { + const subscriberId = await klicktippConnector.subscriberSearch(email) + return klicktippConnector.subscriberUpdate(subscriberId, fields, newemail, newsmsnumber) + } + throw new LogError(`Could not add fields (${fields}) to subscriber ${email}`) + }, + { email, fields, newemail, newsmsnumber }, + ) } From 7df35f057b792cc25ca95a3691b86527878096c0 Mon Sep 17 00:00:00 2001 From: elweyn Date: Tue, 4 Apr 2023 17:07:01 +0200 Subject: [PATCH 032/153] Revert "Implement callKlickTippAPI" This reverts commit 51d572f0b795efa54f771a6d19db8675a1efa5cd. --- backend/src/apis/KlicktippController.ts | 178 ++++++++++-------------- 1 file changed, 75 insertions(+), 103 deletions(-) diff --git a/backend/src/apis/KlicktippController.ts b/backend/src/apis/KlicktippController.ts index d414bb9ec..1278b0d9c 100644 --- a/backend/src/apis/KlicktippController.ts +++ b/backend/src/apis/KlicktippController.ts @@ -4,135 +4,107 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ import CONFIG from '@/config' -import LogError from '@/server/LogError' // eslint-disable-next-line import/no-relative-parent-imports import KlicktippConnector from 'klicktipp-api' const klicktippConnector = new KlicktippConnector() -const callKlickTippAPI = (callback: (arg0: any) => any, args: any) => { - if (!CONFIG.KLICKTIPP) { - return true - } - return callback(args) -} - -export const klicktippSignIn = ( +export const klicktippSignIn = async ( email: string, language: string, firstName?: string, lastName?: string, ): Promise => { - return callKlickTippAPI( - ({ fieldFirstName, fieldLastName, language, email }) => { - const fields = { - fieldFirstName, - fieldLastName, - } - const apiKey = language === 'de' ? CONFIG.KLICKTIPP_APIKEY_DE : CONFIG.KLICKTIPP_APIKEY_EN - return klicktippConnector.signin(apiKey, email, fields) - }, - { - fieldFirstName: firstName, - fieldLastName: lastName, - language, - email, - }, - ) + if (!CONFIG.KLICKTIPP) { + return true + } + const fields = { + fieldFirstName: firstName, + fieldLastName: lastName, + } + const apiKey = language === 'de' ? CONFIG.KLICKTIPP_APIKEY_DE : CONFIG.KLICKTIPP_APIKEY_EN + const result = await klicktippConnector.signin(apiKey, email, fields) + return result } -export const signout = (email: string, language: string): Promise => { - return callKlickTippAPI( - async ({ language, email }) => { - const apiKey = language === 'de' ? CONFIG.KLICKTIPP_APIKEY_DE : CONFIG.KLICKTIPP_APIKEY_EN - const result = await klicktippConnector.signoff(apiKey, email) - return result - }, - { - email, - language, - }, - ) +export const signout = async (email: string, language: string): Promise => { + if (!CONFIG.KLICKTIPP) { + return true + } + const apiKey = language === 'de' ? CONFIG.KLICKTIPP_APIKEY_DE : CONFIG.KLICKTIPP_APIKEY_EN + const result = await klicktippConnector.signoff(apiKey, email) + return result } -export const unsubscribe = (email: string): Promise => { - return callKlickTippAPI( - async ({ email }) => { - const isLogin = await loginKlicktippUser() - if (isLogin) { - return klicktippConnector.unsubscribe(email) - } - throw new LogError(`Could not unsubscribe ${email}`) - }, - { - email, - }, - ) +export const unsubscribe = async (email: string): Promise => { + if (!CONFIG.KLICKTIPP) { + return true + } + const isLogin = await loginKlicktippUser() + if (isLogin) { + return await klicktippConnector.unsubscribe(email) + } + throw new Error(`Could not unsubscribe ${email}`) } -export const getKlickTippUser = (email: string): Promise => { - return callKlickTippAPI( - async ({ email }) => { - const isLogin = await loginKlicktippUser() - if (isLogin) { - const subscriberId = await klicktippConnector.subscriberSearch(email) - return klicktippConnector.subscriberGet(subscriberId) - } - throw new LogError(`Could not get subscriber ${email}`) - }, - { - email, - }, - ) +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 + } + return false } -export const loginKlicktippUser = (): Promise => { - return callKlickTippAPI(() => { - return klicktippConnector.login(CONFIG.KLICKTIPP_USER, CONFIG.KLICKTIPP_PASSWORD) - }, {}) +export const loginKlicktippUser = async (): Promise => { + if (!CONFIG.KLICKTIPP) { + return true + } + return await klicktippConnector.login(CONFIG.KLICKTIPP_USER, CONFIG.KLICKTIPP_PASSWORD) } -export const logoutKlicktippUser = (): Promise => { - return callKlickTippAPI(() => { - return klicktippConnector.logout() - }, {}) +export const logoutKlicktippUser = async (): Promise => { + if (!CONFIG.KLICKTIPP) { + return true + } + return await klicktippConnector.logout() } -export const untagUser = (email: string, tagId: string): Promise => { - return callKlickTippAPI( - async ({ email, tagId }) => { - const isLogin = await loginKlicktippUser() - if (isLogin) { - return await klicktippConnector.untag(email, tagId) - } - throw new LogError(`Could not untag ${email}`) - }, - { email, tagId }, - ) +export const untagUser = async (email: string, tagId: string): Promise => { + if (!CONFIG.KLICKTIPP) { + return true + } + const isLogin = await loginKlicktippUser() + if (isLogin) { + return await klicktippConnector.untag(email, tagId) + } + return false } -export const tagUser = (email: string, tagIds: string): Promise => { - return callKlickTippAPI( - async ({ email, tagIds }) => { - const isLogin = await loginKlicktippUser() - if (isLogin) { - return klicktippConnector.tag(email, tagIds) - } - throw new LogError(`Could not tag ${email}`) - }, - { email, tagIds }, - ) +export const tagUser = async (email: string, tagIds: string): Promise => { + if (!CONFIG.KLICKTIPP) { + return true + } + const isLogin = await loginKlicktippUser() + if (isLogin) { + return await klicktippConnector.tag(email, tagIds) + } + return false } -export const getKlicktippTagMap = (): Promise => { - return callKlickTippAPI(async () => { - const isLogin = await loginKlicktippUser() - if (isLogin) { - return klicktippConnector.tagIndex() - } - throw new LogError(`Could not get tagIndexes`) - }, {}) +export const getKlicktippTagMap = async () => { + if (!CONFIG.KLICKTIPP) { + return '' + } + const isLogin = await loginKlicktippUser() + if (isLogin) { + return await klicktippConnector.tagIndex() + } + return '' } From b00c518863e81e30291eaf10d285efacec1c4b65 Mon Sep 17 00:00:00 2001 From: elweyn Date: Tue, 4 Apr 2023 17:09:32 +0200 Subject: [PATCH 033/153] Implement callKlickTippAPI --- backend/src/apis/KlicktippController.ts | 36 +++++++------------------ 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/backend/src/apis/KlicktippController.ts b/backend/src/apis/KlicktippController.ts index 1278b0d9c..a0fc6e06f 100644 --- a/backend/src/apis/KlicktippController.ts +++ b/backend/src/apis/KlicktippController.ts @@ -17,9 +17,7 @@ export const klicktippSignIn = async ( firstName?: string, lastName?: string, ): Promise => { - if (!CONFIG.KLICKTIPP) { - return true - } + if (!CONFIG.KLICKTIPP) return true const fields = { fieldFirstName: firstName, fieldLastName: lastName, @@ -30,18 +28,14 @@ export const klicktippSignIn = async ( } export const signout = async (email: string, language: string): Promise => { - if (!CONFIG.KLICKTIPP) { - return true - } + if (!CONFIG.KLICKTIPP) return true const apiKey = language === 'de' ? CONFIG.KLICKTIPP_APIKEY_DE : CONFIG.KLICKTIPP_APIKEY_EN const result = await klicktippConnector.signoff(apiKey, email) return result } export const unsubscribe = async (email: string): Promise => { - if (!CONFIG.KLICKTIPP) { - return true - } + if (!CONFIG.KLICKTIPP) return true const isLogin = await loginKlicktippUser() if (isLogin) { return await klicktippConnector.unsubscribe(email) @@ -50,9 +44,7 @@ export const unsubscribe = async (email: string): Promise => { } export const getKlickTippUser = async (email: string): Promise => { - if (!CONFIG.KLICKTIPP) { - return true - } + if (!CONFIG.KLICKTIPP) return true const isLogin = await loginKlicktippUser() if (isLogin) { const subscriberId = await klicktippConnector.subscriberSearch(email) @@ -63,23 +55,17 @@ export const getKlickTippUser = async (email: string): Promise => { } export const loginKlicktippUser = async (): Promise => { - if (!CONFIG.KLICKTIPP) { - return true - } + if (!CONFIG.KLICKTIPP) return true return await klicktippConnector.login(CONFIG.KLICKTIPP_USER, CONFIG.KLICKTIPP_PASSWORD) } export const logoutKlicktippUser = async (): Promise => { - if (!CONFIG.KLICKTIPP) { - return true - } + if (!CONFIG.KLICKTIPP) return true return await klicktippConnector.logout() } export const untagUser = async (email: string, tagId: string): Promise => { - if (!CONFIG.KLICKTIPP) { - return true - } + if (!CONFIG.KLICKTIPP) return true const isLogin = await loginKlicktippUser() if (isLogin) { return await klicktippConnector.untag(email, tagId) @@ -88,9 +74,7 @@ export const untagUser = async (email: string, tagId: string): Promise } export const tagUser = async (email: string, tagIds: string): Promise => { - if (!CONFIG.KLICKTIPP) { - return true - } + if (!CONFIG.KLICKTIPP) return true const isLogin = await loginKlicktippUser() if (isLogin) { return await klicktippConnector.tag(email, tagIds) @@ -99,9 +83,7 @@ export const tagUser = async (email: string, tagIds: string): Promise = } export const getKlicktippTagMap = async () => { - if (!CONFIG.KLICKTIPP) { - return '' - } + if (!CONFIG.KLICKTIPP) return true const isLogin = await loginKlicktippUser() if (isLogin) { return await klicktippConnector.tagIndex() From 070f84f21b2a1605f07d890d397a59481eca86f5 Mon Sep 17 00:00:00 2001 From: elweyn Date: Tue, 4 Apr 2023 19:47:50 +0200 Subject: [PATCH 034/153] Remove callKlickTippAPI method. --- backend/src/apis/KlicktippController.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/backend/src/apis/KlicktippController.ts b/backend/src/apis/KlicktippController.ts index 710e2c611..6d59eaefc 100644 --- a/backend/src/apis/KlicktippController.ts +++ b/backend/src/apis/KlicktippController.ts @@ -97,15 +97,11 @@ export const addFieldsToSubscriber = async ( newemail = '', newsmsnumber = '', ) => { - return callKlickTippAPI( - async ({ email, fields, newemail, newsmsnumber }) => { - const isLogin = await loginKlicktippUser() - if (isLogin) { - const subscriberId = await klicktippConnector.subscriberSearch(email) - return klicktippConnector.subscriberUpdate(subscriberId, fields, newemail, newsmsnumber) - } - throw new LogError(`Could not add fields (${fields}) to subscriber ${email}`) - }, - { email, fields, newemail, newsmsnumber }, - ) + if (!CONFIG.KLICKTIPP) return true + const isLogin = await loginKlicktippUser() + if (isLogin) { + const subscriberId = await klicktippConnector.subscriberSearch(email) + return klicktippConnector.subscriberUpdate(subscriberId, fields, newemail, newsmsnumber) + } + return false } From 6bedba53c48d1c7ef0ab2b5919b0b13fe07af279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 4 Apr 2023 23:12:05 +0200 Subject: [PATCH 035/153] linting --- backend/src/util/virtualTransactions.ts | 82 ++++++++++++------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/backend/src/util/virtualTransactions.ts b/backend/src/util/virtualTransactions.ts index cd73f9feb..d06d85908 100644 --- a/backend/src/util/virtualTransactions.ts +++ b/backend/src/util/virtualTransactions.ts @@ -40,25 +40,25 @@ const virtualLinkTransaction = ( user: User, ): Transaction => { const linkDbTransaction: dbTransaction = { - id: -2, - userId: -1, - previous: -1, - typeId: TransactionTypeId.LINK_SUMMARY, - amount: amount.toDecimalPlaces(2, Decimal.ROUND_FLOOR), - balance: balance.toDecimalPlaces(2, Decimal.ROUND_DOWN), - balanceDate: validUntil, - decayStart: createdAt, - decay: decay.toDecimalPlaces(2, Decimal.ROUND_FLOOR), - memo: '', - creationDate: null, - contribution: null, - ...defaultModelFunctions, - userGradidoID: '', - userCommunityUuid: '', - userName: '', - linkedUserGradidoID: '', - linkedUserCommunityUuid: '', - linkedUserName: '' + id: -2, + userId: -1, + previous: -1, + typeId: TransactionTypeId.LINK_SUMMARY, + amount: amount.toDecimalPlaces(2, Decimal.ROUND_FLOOR), + balance: balance.toDecimalPlaces(2, Decimal.ROUND_DOWN), + balanceDate: validUntil, + decayStart: createdAt, + decay: decay.toDecimalPlaces(2, Decimal.ROUND_FLOOR), + memo: '', + creationDate: null, + contribution: null, + ...defaultModelFunctions, + userGradidoID: '', + userCommunityUuid: '', + userName: '', + linkedUserGradidoID: '', + linkedUserCommunityUuid: '', + linkedUserName: '', } return new Transaction(linkDbTransaction, user) } @@ -73,28 +73,28 @@ const virtualDecayTransaction = ( const decay = calculateDecay(balance, balanceDate, time) // const balance = decay.balance.minus(lastTransaction.balance) const decayDbTransaction: dbTransaction = { - id: -1, - userId: -1, - previous: -1, - typeId: TransactionTypeId.DECAY, - amount: decay.decay ? decay.roundedDecay : new Decimal(0), - balance: decay.balance - .toDecimalPlaces(2, Decimal.ROUND_DOWN) - .minus(holdAvailabeAmount.toString()) - .toDecimalPlaces(2, Decimal.ROUND_DOWN), - balanceDate: time, - decay: decay.decay ? decay.roundedDecay : new Decimal(0), - decayStart: decay.start, - memo: '', - creationDate: null, - contribution: null, - ...defaultModelFunctions, - userGradidoID: '', - userCommunityUuid: '', - userName: '', - linkedUserGradidoID: '', - linkedUserCommunityUuid: '', - linkedUserName: '' + id: -1, + userId: -1, + previous: -1, + typeId: TransactionTypeId.DECAY, + amount: decay.decay ? decay.roundedDecay : new Decimal(0), + balance: decay.balance + .toDecimalPlaces(2, Decimal.ROUND_DOWN) + .minus(holdAvailabeAmount.toString()) + .toDecimalPlaces(2, Decimal.ROUND_DOWN), + balanceDate: time, + decay: decay.decay ? decay.roundedDecay : new Decimal(0), + decayStart: decay.start, + memo: '', + creationDate: null, + contribution: null, + ...defaultModelFunctions, + userGradidoID: '', + userCommunityUuid: '', + userName: '', + linkedUserGradidoID: '', + linkedUserCommunityUuid: '', + linkedUserName: '', } return new Transaction(decayDbTransaction, user) } From 56dbcd4f589ad7f7ddd92e9d4c2b3aac3c88368b Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 5 Apr 2023 05:46:38 +0200 Subject: [PATCH 036/153] various more typesafety --- backend/src/graphql/directive/isAuthorized.ts | 10 +++------- .../graphql/resolver/ContributionResolver.ts | 7 +++---- backend/src/index.ts | 1 - backend/src/seeds/factory/creation.ts | 4 ---- backend/src/seeds/index.ts | 13 ++++--------- backend/src/server/context.ts | 1 + backend/src/server/plugins.ts | 1 - backend/test/helpers.ts | 19 +++++++------------ backend/test/testSetup.ts | 6 ++---- 9 files changed, 20 insertions(+), 42 deletions(-) diff --git a/backend/src/graphql/directive/isAuthorized.ts b/backend/src/graphql/directive/isAuthorized.ts index d659b3f1c..9cddc76ac 100644 --- a/backend/src/graphql/directive/isAuthorized.ts +++ b/backend/src/graphql/directive/isAuthorized.ts @@ -1,8 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ - import { User } from '@entity/User' import { AuthChecker } from 'type-graphql' @@ -10,9 +5,10 @@ import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS' import { decode, encode } from '@/auth/JWT' import { RIGHTS } from '@/auth/RIGHTS' import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN } from '@/auth/ROLES' +import { Context } from '@/server/context' import LogError from '@/server/LogError' -const isAuthorized: AuthChecker = async ({ context }, rights) => { +const isAuthorized: AuthChecker = async ({ context }, rights) => { context.role = ROLE_UNAUTHORIZED // unauthorized user // is rights an inalienable right? @@ -47,7 +43,7 @@ const isAuthorized: AuthChecker = async ({ context }, rights) => { } // check for correct rights - const missingRights = (rights).filter((right) => !context.role.hasRight(right)) + const missingRights = (rights).filter((right) => !context.role?.hasRight(right)) if (missingRights.length !== 0) { throw new LogError('401 Unauthorized') } diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 5969eaef2..a4a68f584 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ import { IsNull, getConnection } from '@dbTools/typeorm' import { Contribution as DbContribution } from '@entity/Contribution' import { ContributionMessage } from '@entity/ContributionMessage' @@ -229,11 +228,11 @@ export class ContributionResolver { contributionMessage.createdAt = contributionToUpdate.updatedAt ? contributionToUpdate.updatedAt : contributionToUpdate.createdAt - const changeMessage = `${contributionToUpdate.contributionDate} + const changeMessage = `${contributionToUpdate.contributionDate.toString()} --- ${contributionToUpdate.memo} --- - ${contributionToUpdate.amount}` + ${contributionToUpdate.amount.toString()}` contributionMessage.message = changeMessage contributionMessage.isModerator = false contributionMessage.userId = user.id @@ -259,7 +258,7 @@ export class ContributionResolver { @Ctx() context: Context, ): Promise { logger.info( - `adminCreateContribution(email=${email}, amount=${amount}, memo=${memo}, creationDate=${creationDate})`, + `adminCreateContribution(email=${email}, amount=${amount.toString()}, memo=${memo}, creationDate=${creationDate})`, ) const clientTimezoneOffset = getClientTimezoneOffset(context) if (!isValidDateString(creationDate)) { diff --git a/backend/src/index.ts b/backend/src/index.ts index 72b627820..3631deca8 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import CONFIG from './config' import { startValidateCommunities } from './federation/validateCommunities' import createServer from './server/createServer' diff --git a/backend/src/seeds/factory/creation.ts b/backend/src/seeds/factory/creation.ts index 6a3aaa3e7..5b4c56c57 100644 --- a/backend/src/seeds/factory/creation.ts +++ b/backend/src/seeds/factory/creation.ts @@ -2,9 +2,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/unbound-method */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ - import { Contribution } from '@entity/Contribution' import { Transaction } from '@entity/Transaction' import { ApolloServerTestClient } from 'apollo-server-testing' @@ -12,7 +9,6 @@ import { ApolloServerTestClient } from 'apollo-server-testing' import { findUserByEmail } from '@/graphql/resolver/UserResolver' import { CreationInterface } from '@/seeds/creation/CreationInterface' import { login, createContribution, confirmContribution } from '@/seeds/graphql/mutations' -// import CONFIG from '@/config/index' export const nMonthsBefore = (date: Date, months = 1): string => { return new Date(date.getFullYear(), date.getMonth() - months, 1).toISOString() diff --git a/backend/src/seeds/index.ts b/backend/src/seeds/index.ts index d1960eea9..df6bf768c 100644 --- a/backend/src/seeds/index.ts +++ b/backend/src/seeds/index.ts @@ -1,10 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ - import { entities } from '@entity/index' import { createTestClient } from 'apollo-server-testing' import { name, internet, datatype } from 'faker' @@ -43,10 +36,12 @@ export const cleanDB = async () => { } } -const resetEntity = async (entity: any) => { +const [entityTypes] = entities + +const resetEntity = async (entity: typeof entityTypes) => { const items = await entity.find({ withDeleted: true }) if (items.length > 0) { - const ids = items.map((i: any) => i.id) + const ids = items.map((i) => i.id) await entity.delete(ids) } } diff --git a/backend/src/server/context.ts b/backend/src/server/context.ts index f0e63daea..af5047348 100644 --- a/backend/src/server/context.ts +++ b/backend/src/server/context.ts @@ -13,6 +13,7 @@ export interface Context { role?: Role user?: dbUser clientTimezoneOffset?: number + gradidoID?: string // hack to use less DB calls for Balance Resolver lastTransaction?: dbTransaction transactionCount?: number diff --git a/backend/src/server/plugins.ts b/backend/src/server/plugins.ts index d113cb4b7..067810cf8 100644 --- a/backend/src/server/plugins.ts +++ b/backend/src/server/plugins.ts @@ -5,7 +5,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ - import clonedeep from 'lodash.clonedeep' const setHeadersPlugin = { diff --git a/backend/test/helpers.ts b/backend/test/helpers.ts index cd86573fc..4944fddc1 100644 --- a/backend/test/helpers.ts +++ b/backend/test/helpers.ts @@ -1,20 +1,13 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* 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/unbound-method */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ - import { entities } from '@entity/index' import { createTestClient } from 'apollo-server-testing' +import { Context } from '@/server/context' import createServer from '@/server/createServer' import { i18n, logger } from './testSetup' -export const headerPushMock = jest.fn((t) => { +export const headerPushMock = jest.fn(([t]: Context['setHeaders']) => { context.token = t.value }) @@ -34,7 +27,7 @@ export const cleanDB = async () => { } } -export const testEnvironment = async (testLogger: any = logger, testI18n: any = i18n) => { +export const testEnvironment = async (testLogger = logger, testI18n = i18n) => { const server = await createServer(context, testLogger, testI18n) const con = server.con const testClient = createTestClient(server.apollo) @@ -43,10 +36,12 @@ export const testEnvironment = async (testLogger: any = logger, testI18n: any = return { mutate, query, con } } -export const resetEntity = async (entity: any) => { +const [entityTypes] = entities + +export const resetEntity = async (entity: typeof entityTypes) => { const items = await entity.find({ withDeleted: true }) if (items.length > 0) { - const ids = items.map((i: any) => i.id) + const ids = items.map((i) => i.id) await entity.delete(ids) } } diff --git a/backend/test/testSetup.ts b/backend/test/testSetup.ts index 4e8a67e3f..2c8afee32 100644 --- a/backend/test/testSetup.ts +++ b/backend/test/testSetup.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ import CONFIG from '@/config' import { i18n } from '@/server/localization' import { backendLogger as logger } from '@/server/logger' @@ -10,7 +8,7 @@ CONFIG.EMAIL_TEST_MODUS = false jest.setTimeout(1000000) jest.mock('@/server/logger', () => { - const originalModule = jest.requireActual('@/server/logger') + const originalModule = jest.requireActual('@/server/logger') return { __esModule: true, ...originalModule, @@ -27,7 +25,7 @@ jest.mock('@/server/logger', () => { }) jest.mock('@/server/localization', () => { - const originalModule = jest.requireActual('@/server/localization') + const originalModule = jest.requireActual('@/server/localization') return { __esModule: true, ...originalModule, From 1505d9abf06163e3d3535c481148d9910e765bef Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 5 Apr 2023 10:43:42 +0200 Subject: [PATCH 037/153] Change ACTION_NEWSLETTER to NEWSLETTER_ACTION --- ...UBSCRIBE_NEWSLETTER.ts => EVENT_NEWSLETTER_SUBSCRIBE.ts} | 4 ++-- ...SCRIBE_NEWSLETTER.ts => EVENT_NEWSLETTER_UNSUBSCRIBE.ts} | 4 ++-- backend/src/event/Event.ts | 4 ++-- backend/src/event/EventType.ts | 4 ++-- backend/src/graphql/resolver/KlicktippResolver.ts | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) rename backend/src/event/{EVENT_SUBSCRIBE_NEWSLETTER.ts => EVENT_NEWSLETTER_SUBSCRIBE.ts} (56%) rename backend/src/event/{EVENT_UNSUBSCRIBE_NEWSLETTER.ts => EVENT_NEWSLETTER_UNSUBSCRIBE.ts} (56%) diff --git a/backend/src/event/EVENT_SUBSCRIBE_NEWSLETTER.ts b/backend/src/event/EVENT_NEWSLETTER_SUBSCRIBE.ts similarity index 56% rename from backend/src/event/EVENT_SUBSCRIBE_NEWSLETTER.ts rename to backend/src/event/EVENT_NEWSLETTER_SUBSCRIBE.ts index a9656b6fa..51fb3d186 100644 --- a/backend/src/event/EVENT_SUBSCRIBE_NEWSLETTER.ts +++ b/backend/src/event/EVENT_NEWSLETTER_SUBSCRIBE.ts @@ -3,5 +3,5 @@ import { User as DbUser } from '@entity/User' import { Event, EventType } from './Event' -export const EVENT_SUBSCRIBE_NEWSLETTER = async (user: DbUser): Promise => - Event(EventType.SUBSCRIBE_NEWSLETTER, user, user).save() +export const EVENT_NEWSLETTER_SUBSCRIBE = async (user: DbUser): Promise => + Event(EventType.NEWSLETTER_SUBSCRIBE, user, user).save() diff --git a/backend/src/event/EVENT_UNSUBSCRIBE_NEWSLETTER.ts b/backend/src/event/EVENT_NEWSLETTER_UNSUBSCRIBE.ts similarity index 56% rename from backend/src/event/EVENT_UNSUBSCRIBE_NEWSLETTER.ts rename to backend/src/event/EVENT_NEWSLETTER_UNSUBSCRIBE.ts index 1b2561894..4e4d3e9e4 100644 --- a/backend/src/event/EVENT_UNSUBSCRIBE_NEWSLETTER.ts +++ b/backend/src/event/EVENT_NEWSLETTER_UNSUBSCRIBE.ts @@ -3,5 +3,5 @@ import { User as DbUser } from '@entity/User' import { Event, EventType } from './Event' -export const EVENT_UNSUBSCRIBE_NEWSLETTER = async (user: DbUser): Promise => - Event(EventType.UNSUBSCRIBE_NEWSLETTER, user, user).save() +export const EVENT_NEWSLETTER_UNSUBSCRIBE = async (user: DbUser): Promise => + Event(EventType.NEWSLETTER_UNSUBSCRIBE, user, user).save() diff --git a/backend/src/event/Event.ts b/backend/src/event/Event.ts index 05c0a78a9..2064a91f0 100644 --- a/backend/src/event/Event.ts +++ b/backend/src/event/Event.ts @@ -58,13 +58,13 @@ export { EVENT_EMAIL_ACCOUNT_MULTIREGISTRATION } from './EVENT_EMAIL_ACCOUNT_MUL export { EVENT_EMAIL_ADMIN_CONFIRMATION } from './EVENT_EMAIL_ADMIN_CONFIRMATION' export { EVENT_EMAIL_CONFIRMATION } from './EVENT_EMAIL_CONFIRMATION' export { EVENT_EMAIL_FORGOT_PASSWORD } from './EVENT_EMAIL_FORGOT_PASSWORD' -export { EVENT_SUBSCRIBE_NEWSLETTER } from './EVENT_SUBSCRIBE_NEWSLETTER' +export { EVENT_NEWSLETTER_SUBSCRIBE } from './EVENT_NEWSLETTER_SUBSCRIBE' export { EVENT_TRANSACTION_SEND } from './EVENT_TRANSACTION_SEND' export { EVENT_TRANSACTION_RECEIVE } from './EVENT_TRANSACTION_RECEIVE' export { EVENT_TRANSACTION_LINK_CREATE } from './EVENT_TRANSACTION_LINK_CREATE' export { EVENT_TRANSACTION_LINK_DELETE } from './EVENT_TRANSACTION_LINK_DELETE' export { EVENT_TRANSACTION_LINK_REDEEM } from './EVENT_TRANSACTION_LINK_REDEEM' -export { EVENT_UNSUBSCRIBE_NEWSLETTER } from './EVENT_UNSUBSCRIBE_NEWSLETTER' +export { EVENT_NEWSLETTER_UNSUBSCRIBE } from './EVENT_NEWSLETTER_UNSUBSCRIBE' export { EVENT_USER_ACTIVATE_ACCOUNT } from './EVENT_USER_ACTIVATE_ACCOUNT' export { EVENT_USER_INFO_UPDATE } from './EVENT_USER_INFO_UPDATE' export { EVENT_USER_LOGIN } from './EVENT_USER_LOGIN' diff --git a/backend/src/event/EventType.ts b/backend/src/event/EventType.ts index 137bbfa09..b2b6f9322 100644 --- a/backend/src/event/EventType.ts +++ b/backend/src/event/EventType.ts @@ -21,13 +21,13 @@ export enum EventType { EMAIL_ADMIN_CONFIRMATION = 'EMAIL_ADMIN_CONFIRMATION', EMAIL_CONFIRMATION = 'EMAIL_CONFIRMATION', EMAIL_FORGOT_PASSWORD = 'EMAIL_FORGOT_PASSWORD', - SUBSCRIBE_NEWSLETTER = 'SUBSCRIBE_NEWSLETTER', + NEWSLETTER_SUBSCRIBE = 'NEWSLETTER_SUBSCRIBE', + NEWSLETTER_UNSUBSCRIBE = 'NEWSLETTER_UNSUBSCRIBE', TRANSACTION_SEND = 'TRANSACTION_SEND', TRANSACTION_RECEIVE = 'TRANSACTION_RECEIVE', TRANSACTION_LINK_CREATE = 'TRANSACTION_LINK_CREATE', TRANSACTION_LINK_DELETE = 'TRANSACTION_LINK_DELETE', TRANSACTION_LINK_REDEEM = 'TRANSACTION_LINK_REDEEM', - UNSUBSCRIBE_NEWSLETTER = 'UNSUBSCRIBE_NEWSLETTER', USER_ACTIVATE_ACCOUNT = 'ACTIVATE_ACCOUNT', USER_INFO_UPDATE = 'USER_INFO_UPDATE', USER_LOGIN = 'USER_LOGIN', diff --git a/backend/src/graphql/resolver/KlicktippResolver.ts b/backend/src/graphql/resolver/KlicktippResolver.ts index 03b591c4d..cb09822cf 100644 --- a/backend/src/graphql/resolver/KlicktippResolver.ts +++ b/backend/src/graphql/resolver/KlicktippResolver.ts @@ -2,7 +2,7 @@ import { Resolver, Authorized, Mutation, Ctx } from 'type-graphql' import { unsubscribe, klicktippSignIn } from '@/apis/KlicktippController' import { RIGHTS } from '@/auth/RIGHTS' -import { EVENT_UNSUBSCRIBE_NEWSLETTER, EVENT_SUBSCRIBE_NEWSLETTER } from '@/event/Event' +import { EVENT_NEWSLETTER_SUBSCRIBE, EVENT_NEWSLETTER_UNSUBSCRIBE } from '@/event/Event' import { Context, getUser } from '@/server/context' @Resolver() @@ -11,7 +11,7 @@ export class KlicktippResolver { @Mutation(() => Boolean) async unsubscribeNewsletter(@Ctx() context: Context): Promise { const user = getUser(context) - await EVENT_UNSUBSCRIBE_NEWSLETTER(user) + await EVENT_NEWSLETTER_UNSUBSCRIBE(user) return unsubscribe(user.emailContact.email) } @@ -19,7 +19,7 @@ export class KlicktippResolver { @Mutation(() => Boolean) async subscribeNewsletter(@Ctx() context: Context): Promise { const user = getUser(context) - await EVENT_SUBSCRIBE_NEWSLETTER(user) + await EVENT_NEWSLETTER_SUBSCRIBE(user) return klicktippSignIn(user.emailContact.email, user.language) } } From 50574ece6b5c364185d8de89e363653a2ec52e5a Mon Sep 17 00:00:00 2001 From: Hannes Heine Date: Thu, 6 Apr 2023 14:52:58 +0200 Subject: [PATCH 038/153] Update backend/src/graphql/resolver/KlicktippResolver.test.ts Co-authored-by: Ulf Gebhardt --- backend/src/graphql/resolver/KlicktippResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/KlicktippResolver.test.ts b/backend/src/graphql/resolver/KlicktippResolver.test.ts index 02b3e0492..79bc2f218 100644 --- a/backend/src/graphql/resolver/KlicktippResolver.test.ts +++ b/backend/src/graphql/resolver/KlicktippResolver.test.ts @@ -119,7 +119,7 @@ describe('KlicktippResolver', () => { expect(isUnsubscribed).toEqual(true) }) - it('stores the UNSUBSCRIBE_NEWSLETTER event in the database', async () => { + it('stores the NEWSLETTER_UNSUBSCRIBE event in the database', async () => { const userConatct = await UserContact.findOneOrFail( { email: 'bibi@bloxberg.de' }, { relations: ['user'] }, From 3990135c7a02dca9e9b6f7e27d4027f6fac92b35 Mon Sep 17 00:00:00 2001 From: Hannes Heine Date: Thu, 6 Apr 2023 14:53:09 +0200 Subject: [PATCH 039/153] Update backend/src/graphql/resolver/KlicktippResolver.test.ts Co-authored-by: Ulf Gebhardt --- backend/src/graphql/resolver/KlicktippResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/KlicktippResolver.test.ts b/backend/src/graphql/resolver/KlicktippResolver.test.ts index 79bc2f218..62dee84b0 100644 --- a/backend/src/graphql/resolver/KlicktippResolver.test.ts +++ b/backend/src/graphql/resolver/KlicktippResolver.test.ts @@ -70,7 +70,7 @@ describe('KlicktippResolver', () => { expect(isSubscribed).toEqual(true) }) - it('stores the SUBSCRIBE_NEWSLETTER event in the database', async () => { + it('stores the NEWSLETTER_SUBSCRIBE event in the database', async () => { const userConatct = await UserContact.findOneOrFail( { email: 'bibi@bloxberg.de' }, { relations: ['user'] }, From f36bc61a7024b55bdc357b859a21d5fbc5a47009 Mon Sep 17 00:00:00 2001 From: Hannes Heine Date: Thu, 6 Apr 2023 14:53:24 +0200 Subject: [PATCH 040/153] Update backend/src/graphql/resolver/KlicktippResolver.test.ts Co-authored-by: Ulf Gebhardt --- backend/src/graphql/resolver/KlicktippResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/KlicktippResolver.test.ts b/backend/src/graphql/resolver/KlicktippResolver.test.ts index 62dee84b0..322131480 100644 --- a/backend/src/graphql/resolver/KlicktippResolver.test.ts +++ b/backend/src/graphql/resolver/KlicktippResolver.test.ts @@ -126,7 +126,7 @@ describe('KlicktippResolver', () => { ) await expect(DbEvent.find()).resolves.toContainEqual( expect.objectContaining({ - type: EventType.UNSUBSCRIBE_NEWSLETTER, + type: EventType.NEWSLETTER_UNSUBSCRIBE, affectedUserId: userConatct.user.id, actingUserId: userConatct.user.id, }), From 7d12ecbe9845570e3f0f71360af65aad4cbd98a1 Mon Sep 17 00:00:00 2001 From: Hannes Heine Date: Thu, 6 Apr 2023 14:53:35 +0200 Subject: [PATCH 041/153] Update backend/src/graphql/resolver/KlicktippResolver.test.ts Co-authored-by: Ulf Gebhardt --- backend/src/graphql/resolver/KlicktippResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/KlicktippResolver.test.ts b/backend/src/graphql/resolver/KlicktippResolver.test.ts index 322131480..20669d242 100644 --- a/backend/src/graphql/resolver/KlicktippResolver.test.ts +++ b/backend/src/graphql/resolver/KlicktippResolver.test.ts @@ -77,7 +77,7 @@ describe('KlicktippResolver', () => { ) await expect(DbEvent.find()).resolves.toContainEqual( expect.objectContaining({ - type: EventType.SUBSCRIBE_NEWSLETTER, + type: EventType.NEWSLETTER_SUBSCRIBE, affectedUserId: userConatct.user.id, actingUserId: userConatct.user.id, }), From 525a560832a3abf498a5e78adbbc19111950fb39 Mon Sep 17 00:00:00 2001 From: Hannes Heine Date: Thu, 6 Apr 2023 14:53:51 +0200 Subject: [PATCH 042/153] Update backend/src/event/Event.ts Co-authored-by: Ulf Gebhardt --- backend/src/event/Event.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/event/Event.ts b/backend/src/event/Event.ts index 2064a91f0..4bebac1c0 100644 --- a/backend/src/event/Event.ts +++ b/backend/src/event/Event.ts @@ -59,12 +59,12 @@ export { EVENT_EMAIL_ADMIN_CONFIRMATION } from './EVENT_EMAIL_ADMIN_CONFIRMATION export { EVENT_EMAIL_CONFIRMATION } from './EVENT_EMAIL_CONFIRMATION' export { EVENT_EMAIL_FORGOT_PASSWORD } from './EVENT_EMAIL_FORGOT_PASSWORD' export { EVENT_NEWSLETTER_SUBSCRIBE } from './EVENT_NEWSLETTER_SUBSCRIBE' +export { EVENT_NEWSLETTER_UNSUBSCRIBE } from './EVENT_NEWSLETTER_UNSUBSCRIBE' export { EVENT_TRANSACTION_SEND } from './EVENT_TRANSACTION_SEND' export { EVENT_TRANSACTION_RECEIVE } from './EVENT_TRANSACTION_RECEIVE' export { EVENT_TRANSACTION_LINK_CREATE } from './EVENT_TRANSACTION_LINK_CREATE' export { EVENT_TRANSACTION_LINK_DELETE } from './EVENT_TRANSACTION_LINK_DELETE' export { EVENT_TRANSACTION_LINK_REDEEM } from './EVENT_TRANSACTION_LINK_REDEEM' -export { EVENT_NEWSLETTER_UNSUBSCRIBE } from './EVENT_NEWSLETTER_UNSUBSCRIBE' export { EVENT_USER_ACTIVATE_ACCOUNT } from './EVENT_USER_ACTIVATE_ACCOUNT' export { EVENT_USER_INFO_UPDATE } from './EVENT_USER_INFO_UPDATE' export { EVENT_USER_LOGIN } from './EVENT_USER_LOGIN' From d9e8247b3c0d6249b9a307d30a29c2afafb441e0 Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 6 Apr 2023 15:12:08 +0200 Subject: [PATCH 043/153] Fix event no-cycle. --- backend/src/event/EVENT_NEWSLETTER_SUBSCRIBE.ts | 3 ++- backend/src/event/EVENT_NEWSLETTER_UNSUBSCRIBE.ts | 3 ++- backend/src/graphql/resolver/KlicktippResolver.test.ts | 3 ++- backend/src/graphql/resolver/KlicktippResolver.ts | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/backend/src/event/EVENT_NEWSLETTER_SUBSCRIBE.ts b/backend/src/event/EVENT_NEWSLETTER_SUBSCRIBE.ts index 51fb3d186..717bb8296 100644 --- a/backend/src/event/EVENT_NEWSLETTER_SUBSCRIBE.ts +++ b/backend/src/event/EVENT_NEWSLETTER_SUBSCRIBE.ts @@ -1,7 +1,8 @@ import { Event as DbEvent } from '@entity/Event' import { User as DbUser } from '@entity/User' -import { Event, EventType } from './Event' +import { Event } from './Event' +import { EventType } from './EventType' export const EVENT_NEWSLETTER_SUBSCRIBE = async (user: DbUser): Promise => Event(EventType.NEWSLETTER_SUBSCRIBE, user, user).save() diff --git a/backend/src/event/EVENT_NEWSLETTER_UNSUBSCRIBE.ts b/backend/src/event/EVENT_NEWSLETTER_UNSUBSCRIBE.ts index 4e4d3e9e4..f8adc69d1 100644 --- a/backend/src/event/EVENT_NEWSLETTER_UNSUBSCRIBE.ts +++ b/backend/src/event/EVENT_NEWSLETTER_UNSUBSCRIBE.ts @@ -1,7 +1,8 @@ import { Event as DbEvent } from '@entity/Event' import { User as DbUser } from '@entity/User' -import { Event, EventType } from './Event' +import { Event } from './Event' +import { EventType } from './EventType' export const EVENT_NEWSLETTER_UNSUBSCRIBE = async (user: DbUser): Promise => Event(EventType.NEWSLETTER_UNSUBSCRIBE, user, user).save() diff --git a/backend/src/graphql/resolver/KlicktippResolver.test.ts b/backend/src/graphql/resolver/KlicktippResolver.test.ts index 20669d242..6a2250bc9 100644 --- a/backend/src/graphql/resolver/KlicktippResolver.test.ts +++ b/backend/src/graphql/resolver/KlicktippResolver.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-call */ @@ -9,7 +10,7 @@ import { GraphQLError } from 'graphql' import { cleanDB, resetToken, testEnvironment } from '@test/helpers' import { logger, i18n as localization } from '@test/testSetup' -import { EventType } from '@/event/Event' +import { EventType } from '@/event/Events' import { userFactory } from '@/seeds/factory/user' import { login, subscribeNewsletter, unsubscribeNewsletter } from '@/seeds/graphql/mutations' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' diff --git a/backend/src/graphql/resolver/KlicktippResolver.ts b/backend/src/graphql/resolver/KlicktippResolver.ts index cb09822cf..6875abcc5 100644 --- a/backend/src/graphql/resolver/KlicktippResolver.ts +++ b/backend/src/graphql/resolver/KlicktippResolver.ts @@ -2,7 +2,7 @@ import { Resolver, Authorized, Mutation, Ctx } from 'type-graphql' import { unsubscribe, klicktippSignIn } from '@/apis/KlicktippController' import { RIGHTS } from '@/auth/RIGHTS' -import { EVENT_NEWSLETTER_SUBSCRIBE, EVENT_NEWSLETTER_UNSUBSCRIBE } from '@/event/Event' +import { EVENT_NEWSLETTER_SUBSCRIBE, EVENT_NEWSLETTER_UNSUBSCRIBE } from '@/event/Events' import { Context, getUser } from '@/server/context' @Resolver() From 71f0f6c7f99cca6106c02a8825f76f2f56c1785f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Thu, 6 Apr 2023 15:59:59 +0200 Subject: [PATCH 044/153] rework PR-comments --- .../graphql/resolver/ContributionResolver.ts | 3 ++- .../resolver/TransactionLinkResolver.ts | 3 ++- .../graphql/resolver/TransactionResolver.ts | 9 +++++---- backend/src/util/utilities.ts | 19 ++++++++++++++++++ backend/src/util/virtualTransactions.ts | 20 +++++++++---------- .../Transaction.ts | 20 +++++++++---------- ...-community-sendcoins-transactions_table.ts | 3 ++- 7 files changed, 50 insertions(+), 27 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 741ed2a83..2bb2e0faf 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -55,6 +55,7 @@ import { } from './util/creations' import { findContributions } from './util/findContributions' import { getLastTransaction } from './util/getLastTransaction' +import { fullName } from '@/util/utilities' @Resolver() export class ContributionResolver { @@ -502,7 +503,7 @@ export class ContributionResolver { transaction.memo = contribution.memo transaction.userId = contribution.userId transaction.userGradidoID = user.gradidoID - transaction.userName = user.firstName + ' ' + user.lastName + transaction.userName = fullName(user.firstName, user.lastName) transaction.previous = lastTransaction ? lastTransaction.id : null transaction.amount = contribution.amount transaction.creationDate = contribution.contributionDate diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 856fc5c28..6490b8c9d 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -40,6 +40,7 @@ import { executeTransaction } from './TransactionResolver' import { getUserCreation, validateContribution } from './util/creations' import { getLastTransaction } from './util/getLastTransaction' import transactionLinkList from './util/transactionLinkList' +import { fullName } from '@/util/utilities' // TODO: do not export, test it inside the resolver export const transactionLinkCode = (date: Date): string => { @@ -267,7 +268,7 @@ export class TransactionLinkResolver { transaction.memo = contribution.memo transaction.userId = contribution.userId transaction.userGradidoID = user.gradidoID - transaction.userName = user.firstName + ' ' + user.lastName + transaction.userName = fullName(user.firstName, user.lastName) transaction.previous = lastTransaction ? lastTransaction.id : null transaction.amount = contribution.amount transaction.creationDate = contribution.contributionDate diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 5414de413..44cf35f7d 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -37,6 +37,7 @@ import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { findUserByIdentifier } from './util/findUserByIdentifier' import { getLastTransaction } from './util/getLastTransaction' import { getTransactionList } from './util/getTransactionList' +import { fullName } from '@/util/utilities' export const executeTransaction = async ( amount: Decimal, @@ -88,10 +89,10 @@ export const executeTransaction = async ( transactionSend.memo = memo transactionSend.userId = sender.id transactionSend.userGradidoID = sender.gradidoID - transactionSend.userName = sender.firstName + ' ' + sender.lastName + transactionSend.userName = fullName(sender.firstName, sender.lastName) transactionSend.linkedUserId = recipient.id transactionSend.linkedUserGradidoID = recipient.gradidoID - transactionSend.linkedUserName = recipient.firstName + ' ' + recipient.lastName + transactionSend.linkedUserName = fullName(recipient.firstName, recipient.lastName) transactionSend.amount = amount.mul(-1) transactionSend.balance = sendBalance.balance transactionSend.balanceDate = receivedCallDate @@ -108,10 +109,10 @@ export const executeTransaction = async ( transactionReceive.memo = memo transactionReceive.userId = recipient.id transactionReceive.userGradidoID = recipient.gradidoID - transactionReceive.userName = recipient.firstName + ' ' + recipient.lastName + transactionReceive.userName = fullName(recipient.firstName, recipient.lastName) transactionReceive.linkedUserId = sender.id transactionReceive.linkedUserGradidoID = sender.gradidoID - transactionReceive.linkedUserName = sender.firstName + ' ' + sender.lastName + transactionReceive.linkedUserName = fullName(sender.firstName, sender.lastName) transactionReceive.amount = amount const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate) transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount diff --git a/backend/src/util/utilities.ts b/backend/src/util/utilities.ts index 2cf53f1e4..9a318f576 100644 --- a/backend/src/util/utilities.ts +++ b/backend/src/util/utilities.ts @@ -14,3 +14,22 @@ export const decimalSeparatorByLanguage = (a: Decimal, language: string): string i18n.setLocale(rememberLocaleToRestore) return result } + +export const fullName = (firstName: string, lastName: string): string => { + return [firstName, lastName].filter(Boolean).join(' ') +} + +export const userName = (f?: string, l?: string): string | null => { + let name: string | null + if(f && l) { + name = f + ' ' + l + } else if (f && !l) { + name = f + } else if (!f && l) { + name = l + } else { + name = null + } + + return name +} \ No newline at end of file diff --git a/backend/src/util/virtualTransactions.ts b/backend/src/util/virtualTransactions.ts index a953552b2..2bc2a591e 100644 --- a/backend/src/util/virtualTransactions.ts +++ b/backend/src/util/virtualTransactions.ts @@ -55,11 +55,11 @@ const virtualLinkTransaction = ( contribution: null, ...defaultModelFunctions, userGradidoID: '', - userCommunityUuid: '', - userName: '', - linkedUserGradidoID: '', - linkedUserCommunityUuid: '', - linkedUserName: '', + userCommunityUuid: null, + userName: null, + linkedUserGradidoID: null, + linkedUserCommunityUuid: null, + linkedUserName: null, } return new Transaction(linkDbTransaction, user) } @@ -91,11 +91,11 @@ const virtualDecayTransaction = ( contribution: null, ...defaultModelFunctions, userGradidoID: '', - userCommunityUuid: '', - userName: '', - linkedUserGradidoID: '', - linkedUserCommunityUuid: '', - linkedUserName: '', + userCommunityUuid: null, + userName: null, + linkedUserGradidoID: null, + linkedUserCommunityUuid: null, + linkedUserName: null, } return new Transaction(decayDbTransaction, user) } diff --git a/database/entity/0065-x-community-sendcoins-transactions_table/Transaction.ts b/database/entity/0065-x-community-sendcoins-transactions_table/Transaction.ts index b3f88f0d1..050661824 100644 --- a/database/entity/0065-x-community-sendcoins-transactions_table/Transaction.ts +++ b/database/entity/0065-x-community-sendcoins-transactions_table/Transaction.ts @@ -86,18 +86,18 @@ export class Transaction extends BaseEntity { @Column({ name: 'user_community_uuid', length: 36, - nullable: false, + nullable: true, collation: 'utf8mb4_unicode_ci', }) - userCommunityUuid: string + userCommunityUuid: string | null @Column({ name: 'user_name', length: 512, - nullable: false, + nullable: true, collation: 'utf8mb4_unicode_ci', }) - userName: string + userName: string | null @Column({ name: 'linked_user_id', @@ -111,26 +111,26 @@ export class Transaction extends BaseEntity { @Column({ name: 'linked_user_gradido_id', length: 36, - nullable: false, + nullable: true, collation: 'utf8mb4_unicode_ci', }) - linkedUserGradidoID: string + linkedUserGradidoID: string | null @Column({ name: 'linked_user_community_uuid', length: 36, - nullable: false, + nullable: true, collation: 'utf8mb4_unicode_ci', }) - linkedUserCommunityUuid: string + linkedUserCommunityUuid: string | null @Column({ name: 'linked_user_name', length: 512, - nullable: false, + nullable: true, collation: 'utf8mb4_unicode_ci', }) - linkedUserName: string + linkedUserName: string | null @Column({ name: 'linked_transaction_id', diff --git a/database/migrations/0065-x-community-sendcoins-transactions_table.ts b/database/migrations/0065-x-community-sendcoins-transactions_table.ts index d3228b8d9..a09d416a7 100644 --- a/database/migrations/0065-x-community-sendcoins-transactions_table.ts +++ b/database/migrations/0065-x-community-sendcoins-transactions_table.ts @@ -68,10 +68,11 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis await queryFn( 'ALTER TABLE `transactions` MODIFY COLUMN `user_gradido_id` char(36) NOT NULL AFTER `user_id`;', ) + /* await queryFn( 'ALTER TABLE `transactions` MODIFY COLUMN `user_name` varchar(512) COLLATE utf8mb4_unicode_ci NOT NULL AFTER `user_community_uuid`;', ) - + */ await queryFn( `UPDATE transactions t, users u SET t.linked_user_gradido_id = u.gradido_id, t.linked_user_name = concat(u.first_name, ' ', u.last_name) WHERE t.linked_user_id = u.id and t.linked_user_id is null and t.linked_user_gradido_id is null;`, ) From c62532c89ae2acf7beef94c4127e2ca6bc8884b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Mon, 17 Apr 2023 12:45:20 +0200 Subject: [PATCH 045/153] linting --- backend/src/graphql/resolver/ContributionResolver.ts | 2 +- backend/src/graphql/resolver/TransactionLinkResolver.ts | 2 +- backend/src/graphql/resolver/TransactionResolver.ts | 2 +- backend/src/util/utilities.ts | 8 ++++---- .../Transaction.ts | 4 ++++ 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 357908bdc..696ed9ea7 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -44,6 +44,7 @@ import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' import { calculateDecay } from '@/util/decay' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' +import { fullName } from '@/util/utilities' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { @@ -55,7 +56,6 @@ import { } from './util/creations' import { findContributions } from './util/findContributions' import { getLastTransaction } from './util/getLastTransaction' -import { fullName } from '@/util/utilities' @Resolver() export class ContributionResolver { diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 914b2c081..a97078f66 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -34,13 +34,13 @@ import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' import { calculateDecay } from '@/util/decay' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' +import { fullName } from '@/util/utilities' import { calculateBalance } from '@/util/validate' import { executeTransaction } from './TransactionResolver' import { getUserCreation, validateContribution } from './util/creations' import { getLastTransaction } from './util/getLastTransaction' import { transactionLinkList } from './util/transactionLinkList' -import { fullName } from '@/util/utilities' // TODO: do not export, test it inside the resolver export const transactionLinkCode = (date: Date): string => { diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 65aa29435..7e2a10404 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -29,6 +29,7 @@ import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' import { communityUser } from '@/util/communityUser' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' +import { fullName } from '@/util/utilities' import { calculateBalance } from '@/util/validate' import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions' @@ -37,7 +38,6 @@ import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { findUserByIdentifier } from './util/findUserByIdentifier' import { getLastTransaction } from './util/getLastTransaction' import { getTransactionList } from './util/getTransactionList' -import { fullName } from '@/util/utilities' export const executeTransaction = async ( amount: Decimal, diff --git a/backend/src/util/utilities.ts b/backend/src/util/utilities.ts index 9a318f576..4dc71ff7f 100644 --- a/backend/src/util/utilities.ts +++ b/backend/src/util/utilities.ts @@ -16,12 +16,12 @@ export const decimalSeparatorByLanguage = (a: Decimal, language: string): string } export const fullName = (firstName: string, lastName: string): string => { - return [firstName, lastName].filter(Boolean).join(' ') + return [firstName, lastName].filter(Boolean).join(' ') } export const userName = (f?: string, l?: string): string | null => { let name: string | null - if(f && l) { + if (f && l) { name = f + ' ' + l } else if (f && !l) { name = f @@ -30,6 +30,6 @@ export const userName = (f?: string, l?: string): string | null => { } else { name = null } - + return name -} \ No newline at end of file +} diff --git a/database/entity/0065-x-community-sendcoins-transactions_table/Transaction.ts b/database/entity/0065-x-community-sendcoins-transactions_table/Transaction.ts index 050661824..0da9a2265 100644 --- a/database/entity/0065-x-community-sendcoins-transactions_table/Transaction.ts +++ b/database/entity/0065-x-community-sendcoins-transactions_table/Transaction.ts @@ -144,4 +144,8 @@ export class Transaction extends BaseEntity { @OneToOne(() => Contribution, (contribution) => contribution.transaction) @JoinColumn({ name: 'id', referencedColumnName: 'transactionId' }) contribution?: Contribution | null + + @OneToOne(() => Transaction) + @JoinColumn({ name: 'previous' }) + previousTransaction?: Transaction | null } From 0d61788e958a46661de6e09fe8d0349f9f1755da Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 18 Apr 2023 12:49:40 +0200 Subject: [PATCH 046/153] remove email from store, add gradido ID --- frontend/src/store/store.js | 10 +++++----- frontend/src/store/store.test.js | 22 +++++++++++----------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js index 1cd874c06..436a2d32b 100644 --- a/frontend/src/store/store.js +++ b/frontend/src/store/store.js @@ -13,8 +13,8 @@ export const mutations = { localeChanged(language) state.language = language }, - email: (state, email) => { - state.email = email + gradidoID: (state, gradidoID) => { + state.gradidoID = gradidoID }, // username: (state, username) => { // state.username = username @@ -57,7 +57,7 @@ export const mutations = { export const actions = { login: ({ dispatch, commit }, data) => { - commit('email', data.email) + commit('gradidoID', data.gradidoID) commit('language', data.language) // commit('username', data.username) commit('firstName', data.firstName) @@ -71,8 +71,8 @@ export const actions = { }, logout: ({ commit, state }) => { commit('token', null) - commit('email', null) // commit('username', '') + commit('gradidoID', null) commit('firstName', '') commit('lastName', '') commit('newsletterState', null) @@ -95,8 +95,8 @@ try { }), ], state: { - email: '', language: null, + gradidoID: null, firstName: '', lastName: '', // username: '', diff --git a/frontend/src/store/store.test.js b/frontend/src/store/store.test.js index 33fedd562..cc4d6a284 100644 --- a/frontend/src/store/store.test.js +++ b/frontend/src/store/store.test.js @@ -22,7 +22,7 @@ i18n.locale = 'blubb' const { language, - email, + gradidoID, token, firstName, lastName, @@ -53,11 +53,11 @@ describe('Vuex store', () => { }) }) - describe('email', () => { - it('sets the state of email', () => { - const state = { email: 'nobody@knows.tv' } - email(state, 'someone@there.is') - expect(state.email).toEqual('someone@there.is') + describe('gradidoID', () => { + it('sets the state of gradidoID', () => { + const state = { gradidoID: 'old-id' } + gradidoID(state, 'new-id') + expect(state.gradidoID).toEqual('new-id') }) }) @@ -164,7 +164,7 @@ describe('Vuex store', () => { const commit = jest.fn() const state = {} const commitedData = { - email: 'user@example.org', + gradidoID: 'my-gradido-id', language: 'de', firstName: 'Peter', lastName: 'Lustig', @@ -183,9 +183,9 @@ describe('Vuex store', () => { expect(commit).toHaveBeenCalledTimes(10) }) - it('commits email', () => { + it('commits gradidoID', () => { login({ commit, state }, commitedData) - expect(commit).toHaveBeenNthCalledWith(1, 'email', 'user@example.org') + expect(commit).toHaveBeenNthCalledWith(1, 'gradidoID', 'my-gradido-id') }) it('commits language', () => { @@ -248,9 +248,9 @@ describe('Vuex store', () => { expect(commit).toHaveBeenNthCalledWith(1, 'token', null) }) - it('commits email', () => { + it('commits gradidoID', () => { logout({ commit, state }) - expect(commit).toHaveBeenNthCalledWith(2, 'email', null) + expect(commit).toHaveBeenNthCalledWith(2, 'gradidoID', null) }) it('commits firstName', () => { From 820b4b506bbafd4e62e9520cddbf758d3399e0b6 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 18 Apr 2023 12:49:59 +0200 Subject: [PATCH 047/153] remove all queries for emails --- frontend/src/graphql/mutations.js | 1 - frontend/src/graphql/queries.js | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/graphql/mutations.js b/frontend/src/graphql/mutations.js index 65bdd7497..802ea1818 100644 --- a/frontend/src/graphql/mutations.js +++ b/frontend/src/graphql/mutations.js @@ -145,7 +145,6 @@ export const login = gql` mutation($email: String!, $password: String!, $publisherId: Int) { login(email: $email, password: $password, publisherId: $publisherId) { gradidoID - email firstName lastName language diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index d0cbc145d..38aa2224e 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -3,7 +3,6 @@ import gql from 'graphql-tag' export const verifyLogin = gql` query { verifyLogin { - email firstName lastName language @@ -40,7 +39,6 @@ export const transactionsQuery = gql` firstName lastName gradidoID - email } decay { decay @@ -102,9 +100,9 @@ export const queryTransactionLink = gql` redeemedAt deletedAt user { + gradidoID firstName publisherId - email } } ... on ContributionLink { From a8d9be180233a71dcb48d5a7a974d7910ccbd6ef Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 18 Apr 2023 12:53:39 +0200 Subject: [PATCH 048/153] test for same user via gradido ID instead of email --- frontend/src/pages/TransactionLink.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/TransactionLink.vue b/frontend/src/pages/TransactionLink.vue index 0aa4cfa7d..adc3cb50b 100644 --- a/frontend/src/pages/TransactionLink.vue +++ b/frontend/src/pages/TransactionLink.vue @@ -139,7 +139,7 @@ export default { if (this.tokenExpiresInSeconds < 5) return `LOGGED_OUT` // logged in, nicht berechtigt einzulösen, eigener link - if (this.linkData.user && this.$store.state.email === this.linkData.user.email) { + if (this.linkData.user && this.$store.state.gradidoID === this.linkData.user.gradidoID) { return `SELF_CREATOR` } From 9a71d0b3e2dae19dcf9ac174dbe30f2834353b5b Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 18 Apr 2023 13:17:59 +0200 Subject: [PATCH 049/153] no email in transaktion link test --- frontend/src/pages/TransactionLink.spec.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/src/pages/TransactionLink.spec.js b/frontend/src/pages/TransactionLink.spec.js index adbb25226..0b9cbb5ef 100644 --- a/frontend/src/pages/TransactionLink.spec.js +++ b/frontend/src/pages/TransactionLink.spec.js @@ -32,7 +32,7 @@ apolloQueryMock.mockResolvedValue({ validUntil: transactionLinkValidExpireDate(), redeemedAt: '2022-03-18T10:08:43.000Z', deletedAt: null, - user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' }, + user: { firstName: 'Bibi', publisherId: 0, gradidoID: 'other-user-id' }, }, }, }) @@ -44,7 +44,7 @@ const mocks = { state: { token: null, tokenTime: null, - email: 'bibi@bloxberg.de', + gradidoID: 'current-user-id', }, }, $apollo: { @@ -101,7 +101,7 @@ describe('TransactionLink', () => { validUntil: transactionLinkValidExpireDate(), redeemedAt: '2022-03-18T10:08:43.000Z', deletedAt: now, - user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' }, + user: { firstName: 'Bibi', publisherId: 0, gradidoID: 'other-user-id' }, }, }, }) @@ -132,7 +132,7 @@ describe('TransactionLink', () => { validUntil: '2020-03-18T10:08:43.000Z', redeemedAt: '2022-03-18T10:08:43.000Z', deletedAt: null, - user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' }, + user: { firstName: 'Bibi', publisherId: 0, gradidoID: 'other-user-id' }, }, }, }) @@ -163,7 +163,7 @@ describe('TransactionLink', () => { validUntil: transactionLinkValidExpireDate(), redeemedAt: '2022-03-18T10:08:43.000Z', deletedAt: null, - user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' }, + user: { firstName: 'Bibi', publisherId: 0, gradidoID: 'other-user-id' }, }, }, }) @@ -195,7 +195,7 @@ describe('TransactionLink', () => { validUntil: transactionLinkValidExpireDate(), redeemedAt: null, deletedAt: null, - user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' }, + user: { firstName: 'Bibi', publisherId: 0, gradidoID: 'other-user-id' }, }, }, }) @@ -239,7 +239,7 @@ describe('TransactionLink', () => { validUntil: transactionLinkValidExpireDate(), redeemedAt: null, deletedAt: null, - user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' }, + user: { firstName: 'Bibi', publisherId: 0, gradidoID: 'current-user-id' }, }, }, }) @@ -275,7 +275,7 @@ describe('TransactionLink', () => { validUntil: transactionLinkValidExpireDate(), redeemedAt: null, deletedAt: null, - user: { firstName: 'Peter', publisherId: 0, email: 'peter@listig.de' }, + user: { firstName: 'Peter', publisherId: 0, gradidoID: 'other-user-id' }, }, }, }) @@ -351,7 +351,7 @@ describe('TransactionLink', () => { validUntil: transactionLinkValidExpireDate(), redeemedAt: null, deletedAt: null, - user: { firstName: 'Bibi', publisherId: 0, email: 'bibi@bloxberg.de' }, + user: { firstName: 'Bibi', publisherId: 0, gradidoID: 'other-user-id' }, }, }, }) From fce0f04ab677a501014988e457432e5e646c3f3c Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 18 Apr 2023 13:33:18 +0200 Subject: [PATCH 050/153] remove unused parts of component --- .../TransactionRows/AmountAndNameRow.spec.js | 32 --------------- .../TransactionRows/AmountAndNameRow.vue | 40 +------------------ 2 files changed, 2 insertions(+), 70 deletions(-) diff --git a/frontend/src/components/TransactionRows/AmountAndNameRow.spec.js b/frontend/src/components/TransactionRows/AmountAndNameRow.spec.js index 172f5f401..747cefe64 100644 --- a/frontend/src/components/TransactionRows/AmountAndNameRow.spec.js +++ b/frontend/src/components/TransactionRows/AmountAndNameRow.spec.js @@ -39,37 +39,5 @@ describe('AmountAndNameRow', () => { expect(wrapper.find('div.gdd-transaction-list-item-name').find('a').exists()).toBe(false) }) }) - - describe('with linked user', () => { - beforeEach(async () => { - await wrapper.setProps({ - linkedUser: { firstName: 'Bibi', lastName: 'Bloxberg', email: 'bibi@bloxberg.de' }, - }) - }) - - it('has a link with first and last name', () => { - expect(wrapper.find('div.gdd-transaction-list-item-name').text()).toBe('Bibi Bloxberg') - }) - - it('has a link', () => { - expect(wrapper.find('div.gdd-transaction-list-item-name').find('a').exists()).toBe(true) - }) - - describe('click link', () => { - beforeEach(async () => { - await wrapper.find('div.gdd-transaction-list-item-name').find('a').trigger('click') - }) - - it('emits set tunneled email', () => { - expect(wrapper.emitted('set-tunneled-email')).toEqual([['bibi@bloxberg.de']]) - }) - - it('pushes the route with query for email', () => { - expect(mocks.$router.push).toBeCalledWith({ - path: '/send', - }) - }) - }) - }) }) }) diff --git a/frontend/src/components/TransactionRows/AmountAndNameRow.vue b/frontend/src/components/TransactionRows/AmountAndNameRow.vue index eb68d9f37..530fdf0e8 100644 --- a/frontend/src/components/TransactionRows/AmountAndNameRow.vue +++ b/frontend/src/components/TransactionRows/AmountAndNameRow.vue @@ -10,21 +10,7 @@
- - - {{ itemText }} - - - {{ itemText }} - - {{ $t('via_link') }} - - + {{ text }}
@@ -38,31 +24,9 @@ export default { type: String, required: true, }, - linkedUser: { - type: Object, - required: false, - }, text: { type: String, - required: false, - }, - linkId: { - type: Number, - required: false, - default: null, - }, - }, - methods: { - tunnelEmail() { - this.$emit('set-tunneled-email', this.linkedUser.email) - this.$router.push({ path: '/send' }) - }, - }, - computed: { - itemText() { - return this.linkedUser - ? this.linkedUser.firstName + ' ' + this.linkedUser.lastName - : this.text + required: true, }, }, } From a09f1b3800f9437627b08a29cfdfc915ff1514bc Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 18 Apr 2023 13:42:56 +0200 Subject: [PATCH 051/153] remove further emails --- .../src/components/LanguageSwitch.spec.js | 2 +- frontend/src/components/LanguageSwitch.vue | 2 +- .../src/components/LanguageSwitch2.spec.js | 2 +- frontend/src/components/LanguageSwitch2.vue | 2 +- frontend/src/components/Menu/Navbar.spec.js | 6 ++-- .../UserSettings/UserNewsletter.spec.js | 1 - frontend/src/layouts/DashboardLayout.spec.js | 29 ------------------- 7 files changed, 7 insertions(+), 37 deletions(-) diff --git a/frontend/src/components/LanguageSwitch.spec.js b/frontend/src/components/LanguageSwitch.spec.js index 7f37c535a..6c733de7d 100644 --- a/frontend/src/components/LanguageSwitch.spec.js +++ b/frontend/src/components/LanguageSwitch.spec.js @@ -15,7 +15,7 @@ describe('LanguageSwitch', () => { let wrapper const state = { - email: 'he@ho.he', + gradidoID: 'current-user-id', language: null, } diff --git a/frontend/src/components/LanguageSwitch.vue b/frontend/src/components/LanguageSwitch.vue index 9d901ae9f..38194ba9c 100644 --- a/frontend/src/components/LanguageSwitch.vue +++ b/frontend/src/components/LanguageSwitch.vue @@ -31,7 +31,7 @@ export default { async saveLocale(locale) { // if (this.$i18n.locale === locale) return this.setLocale(locale) - if (this.$store.state.email) { + if (this.$store.state.gradidoID) { this.$apollo .mutate({ mutation: updateUserInfos, diff --git a/frontend/src/components/LanguageSwitch2.spec.js b/frontend/src/components/LanguageSwitch2.spec.js index 0d2b485ec..e7ff6e8c6 100644 --- a/frontend/src/components/LanguageSwitch2.spec.js +++ b/frontend/src/components/LanguageSwitch2.spec.js @@ -15,7 +15,7 @@ describe('LanguageSwitch', () => { let wrapper const state = { - email: 'he@ho.he', + gradidoID: 'current-user-id', language: null, } diff --git a/frontend/src/components/LanguageSwitch2.vue b/frontend/src/components/LanguageSwitch2.vue index d398d2fe0..46b2a5277 100644 --- a/frontend/src/components/LanguageSwitch2.vue +++ b/frontend/src/components/LanguageSwitch2.vue @@ -59,7 +59,7 @@ export default { async saveLocale(locale) { if (this.$i18n.locale === locale) return this.setLocale(locale) - if (this.$store.state.email) { + if (this.$store.state.gradidoID) { this.$apollo .mutate({ mutation: updateUserInfos, diff --git a/frontend/src/components/Menu/Navbar.spec.js b/frontend/src/components/Menu/Navbar.spec.js index 1e05df71d..a942c2644 100644 --- a/frontend/src/components/Menu/Navbar.spec.js +++ b/frontend/src/components/Menu/Navbar.spec.js @@ -20,7 +20,7 @@ const mocks = { state: { firstName: 'Testy', lastName: 'User', - email: 'testy.user@example.com', + gradidoID: 'current-user-id', }, }, } @@ -64,8 +64,8 @@ describe('AuthNavbar', () => { ) }) - it('has the email address', () => { - // expect(wrapper.find('div.small:nth-child(2)').text()).toBe(wrapper.vm.$store.state.email) + // I think this should be username + it.skip('has the email address', () => { expect(wrapper.find('div[data-test="navbar-item-email"]').text()).toBe( wrapper.vm.$store.state.email, ) diff --git a/frontend/src/components/UserSettings/UserNewsletter.spec.js b/frontend/src/components/UserSettings/UserNewsletter.spec.js index b211ec169..7ae3ddd7c 100644 --- a/frontend/src/components/UserSettings/UserNewsletter.spec.js +++ b/frontend/src/components/UserSettings/UserNewsletter.spec.js @@ -18,7 +18,6 @@ describe('UserCard_Newsletter', () => { $store: { state: { language: 'de', - email: 'peter@lustig.de', newsletterState: true, }, commit: storeCommitMock, diff --git a/frontend/src/layouts/DashboardLayout.spec.js b/frontend/src/layouts/DashboardLayout.spec.js index 9f68199da..a2a666591 100644 --- a/frontend/src/layouts/DashboardLayout.spec.js +++ b/frontend/src/layouts/DashboardLayout.spec.js @@ -43,7 +43,6 @@ const mocks = { $store: { dispatch: storeDispatchMock, state: { - email: 'user@example.org', publisherId: 123, firstName: 'User', lastName: 'Example', @@ -260,34 +259,6 @@ describe('DashboardLayout', () => { }) }) - describe.skip('elopage URI', () => { - describe('user has no publisher ID and no elopage', () => { - beforeEach(() => { - mocks.$store.state.publisherId = null - mocks.$store.state.hasElopage = false - wrapper = Wrapper() - }) - - it('links to basic-de', () => { - expect(wrapper.vm.elopageUri).toBe( - 'https://elopage.com/s/gradido/basic-de/payment?locale=en&prid=111&pid=2896&firstName=User&lastName=Example&email=user@example.org', - ) - }) - }) - - describe('user has elopage', () => { - beforeEach(() => { - mocks.$store.state.publisherId = '123' - mocks.$store.state.hasElopage = true - wrapper = Wrapper() - }) - - it('links to sign in for elopage', () => { - expect(wrapper.vm.elopageUri).toBe('https://elopage.com/s/gradido/sign_in?locale=en') - }) - }) - }) - describe.skip('admin method', () => { const windowLocationMock = jest.fn() beforeEach(() => { From 457f4b82443c12ffbe913aa662839d3152fb7251 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 18 Apr 2023 13:48:02 +0200 Subject: [PATCH 052/153] gradidoID on verify login --- frontend/src/graphql/queries.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 38aa2224e..a21117ac2 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -3,6 +3,7 @@ import gql from 'graphql-tag' export const verifyLogin = gql` query { verifyLogin { + gradidoID firstName lastName language From 311bb50eb8839adc999df346b999b90c5e2ad3dc Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 18 Apr 2023 16:14:44 +0200 Subject: [PATCH 053/153] refactor(backend): no email in user --- backend/src/graphql/model/User.ts | 14 -------------- backend/src/graphql/resolver/UserResolver.ts | 3 +-- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index c705ba912..5abbdadb7 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -2,7 +2,6 @@ import { User as dbUser } from '@entity/User' import { ObjectType, Field, Int } from 'type-graphql' import { KlickTipp } from './KlickTipp' -import { UserContact } from './UserContact' @ObjectType() export class User { @@ -10,10 +9,7 @@ export class User { this.id = user.id this.gradidoID = user.gradidoID this.alias = user.alias - this.emailId = user.emailId if (user.emailContact) { - this.email = user.emailContact.email - this.emailContact = new UserContact(user.emailContact) this.emailChecked = user.emailContact.emailChecked } this.firstName = user.firstName @@ -38,16 +34,6 @@ export class User { @Field(() => String, { nullable: true }) alias: string | null - @Field(() => Int, { nullable: true }) - emailId: number | null - - // TODO privacy issue here - @Field(() => String, { nullable: true }) - email: string | null - - @Field(() => UserContact) - emailContact: UserContact - @Field(() => String, { nullable: true }) firstName: string | null diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 60b4403af..23b3c6aa4 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -130,7 +130,7 @@ export class UserResolver { // Elopage Status & Stored PublisherId user.hasElopage = await this.hasElopage(context) - logger.debug(`verifyLogin... successful: ${user.firstName}.${user.lastName}, ${user.email}`) + logger.debug(`verifyLogin... successful: ${user.firstName}.${user.lastName}`) return user } @@ -238,7 +238,6 @@ export class UserResolver { const user = new User(communityDbUser) user.id = sodium.randombytes_random() % (2048 * 16) // TODO: for a better faking derive id from email so that it will be always the same id when the same email comes in? user.gradidoID = uuidv4() - user.email = email user.firstName = firstName user.lastName = lastName user.language = language From 8370b4f27373ef3eb6de5e28554a9d5486063c42 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 18 Apr 2023 16:25:02 +0200 Subject: [PATCH 054/153] test user resolver returns no emails --- backend/src/graphql/resolver/UserResolver.test.ts | 5 +---- backend/src/seeds/graphql/mutations.ts | 1 - backend/src/seeds/graphql/queries.ts | 1 - 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 77b08f0d6..f90f0e66c 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -674,7 +674,6 @@ describe('UserResolver', () => { expect.objectContaining({ data: { login: { - email: 'bibi@bloxberg.de', firstName: 'Bibi', hasElopage: false, id: expect.any(Number), @@ -947,7 +946,6 @@ describe('UserResolver', () => { expect.objectContaining({ data: { verifyLogin: { - email: 'bibi@bloxberg.de', firstName: 'Bibi', lastName: 'Bloxberg', language: 'de', @@ -1304,7 +1302,7 @@ describe('UserResolver', () => { expect.objectContaining({ data: { login: expect.objectContaining({ - email: 'bibi@bloxberg.de', + firstName: 'Benjamin', }), }, }), @@ -1451,7 +1449,6 @@ describe('UserResolver', () => { expect.objectContaining({ data: { login: { - email: 'bibi@bloxberg.de', firstName: 'Bibi', hasElopage: false, id: expect.any(Number), diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index b0716ff74..7de6fb520 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -305,7 +305,6 @@ export const login = gql` mutation ($email: String!, $password: String!, $publisherId: Int) { login(email: $email, password: $password, publisherId: $publisherId) { id - email firstName lastName language diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 8da5211a4..cc1edbc9d 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -3,7 +3,6 @@ import { gql } from 'graphql-tag' export const verifyLogin = gql` query { verifyLogin { - email firstName lastName language From 4a33703f054853d6cf8d410d3f5d5afd6d050714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Wed, 19 Apr 2023 01:50:53 +0200 Subject: [PATCH 055/153] linting --- .../migrations/0065-x-community-sendcoins-transactions_table.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/database/migrations/0065-x-community-sendcoins-transactions_table.ts b/database/migrations/0065-x-community-sendcoins-transactions_table.ts index a09d416a7..9f80460ed 100644 --- a/database/migrations/0065-x-community-sendcoins-transactions_table.ts +++ b/database/migrations/0065-x-community-sendcoins-transactions_table.ts @@ -105,7 +105,6 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis \`deleted_at\` datetime(3) NULL DEFAULT NULL, PRIMARY KEY (\`id\`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`) - } /* eslint-disable @typescript-eslint/no-empty-function */ From 9c6296610963623f8c5bf122e2b481848fb47688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Wed, 19 Apr 2023 23:03:27 +0200 Subject: [PATCH 056/153] add explicit type: 'varchar' for string columns in transaction entity --- .../Transaction.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/database/entity/0065-x-community-sendcoins-transactions_table/Transaction.ts b/database/entity/0065-x-community-sendcoins-transactions_table/Transaction.ts index 0da9a2265..9202f9fd3 100644 --- a/database/entity/0065-x-community-sendcoins-transactions_table/Transaction.ts +++ b/database/entity/0065-x-community-sendcoins-transactions_table/Transaction.ts @@ -77,6 +77,7 @@ export class Transaction extends BaseEntity { @Column({ name: 'user_gradido_id', + type: 'varchar', length: 36, nullable: false, collation: 'utf8mb4_unicode_ci', @@ -85,6 +86,7 @@ export class Transaction extends BaseEntity { @Column({ name: 'user_community_uuid', + type: 'varchar', length: 36, nullable: true, collation: 'utf8mb4_unicode_ci', @@ -93,6 +95,7 @@ export class Transaction extends BaseEntity { @Column({ name: 'user_name', + type: 'varchar', length: 512, nullable: true, collation: 'utf8mb4_unicode_ci', @@ -110,6 +113,7 @@ export class Transaction extends BaseEntity { @Column({ name: 'linked_user_gradido_id', + type: 'varchar', length: 36, nullable: true, collation: 'utf8mb4_unicode_ci', @@ -118,6 +122,7 @@ export class Transaction extends BaseEntity { @Column({ name: 'linked_user_community_uuid', + type: 'varchar', length: 36, nullable: true, collation: 'utf8mb4_unicode_ci', @@ -126,6 +131,7 @@ export class Transaction extends BaseEntity { @Column({ name: 'linked_user_name', + type: 'varchar', length: 512, nullable: true, collation: 'utf8mb4_unicode_ci', From 7fb7ff9e8cb542ca044c32c66227177a24510feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Thu, 20 Apr 2023 02:28:29 +0200 Subject: [PATCH 057/153] rename communities to federated_communities --- backend/src/config/index.ts | 2 +- .../federation/client/1_0/FederationClient.ts | 6 +- .../federation/client/1_1/FederationClient.ts | 6 +- backend/src/federation/validateCommunities.ts | 23 ++++--- backend/src/graphql/model/Community.ts | 37 +++++------- .../src/graphql/model/FederatedCommunity.ts | 45 ++++++++++++++ .../resolver/CommunityResolver.test.ts | 40 ++++++------- .../src/graphql/resolver/CommunityResolver.ts | 24 ++++++-- .../Community.ts | 60 +++++++++++++++++++ .../FederatedCommunity.ts | 51 ++++++++++++++++ database/entity/Community.ts | 2 +- database/entity/FederatedCommunity.ts | 1 + database/entity/index.ts | 2 + .../0065-refactor_communities_table.ts | 37 ++++++++++++ dht-node/src/config/index.ts | 2 +- dht-node/src/dht_node/index.test.ts | 18 +++--- dht-node/src/dht_node/index.ts | 16 ++--- federation/src/config/index.ts | 2 +- .../1_0/resolver/PublicKeyResolver.test.ts | 8 +-- .../api/1_0/resolver/PublicKeyResolver.ts | 4 +- 20 files changed, 301 insertions(+), 85 deletions(-) create mode 100644 backend/src/graphql/model/FederatedCommunity.ts create mode 100644 database/entity/0065-refactor_communities_table/Community.ts create mode 100644 database/entity/0065-refactor_communities_table/FederatedCommunity.ts create mode 100644 database/entity/FederatedCommunity.ts create mode 100644 database/migrations/0065-refactor_communities_table.ts diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 0c36e4d5a..fc88011ea 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -12,7 +12,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0064-event_rename', + DB_VERSION: '0065-refactor_communities_table', 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 diff --git a/backend/src/federation/client/1_0/FederationClient.ts b/backend/src/federation/client/1_0/FederationClient.ts index 13f05e761..743d17348 100644 --- a/backend/src/federation/client/1_0/FederationClient.ts +++ b/backend/src/federation/client/1_0/FederationClient.ts @@ -1,14 +1,16 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { Community as DbCommunity } from '@entity/Community' +import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { gql } from 'graphql-request' import { GraphQLGetClient } from '@/federation/client/GraphQLGetClient' import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' -export async function requestGetPublicKey(dbCom: DbCommunity): Promise { +export async function requestGetPublicKey( + dbCom: DbFederatedCommunity, +): Promise { let endpoint = dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/' endpoint = `${endpoint}${dbCom.apiVersion}/` logger.info(`requestGetPublicKey with endpoint='${endpoint}'...`) diff --git a/backend/src/federation/client/1_1/FederationClient.ts b/backend/src/federation/client/1_1/FederationClient.ts index bda185fba..35c88bf3b 100644 --- a/backend/src/federation/client/1_1/FederationClient.ts +++ b/backend/src/federation/client/1_1/FederationClient.ts @@ -1,14 +1,16 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import { Community as DbCommunity } from '@entity/Community' +import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { gql } from 'graphql-request' import { GraphQLGetClient } from '@/federation/client/GraphQLGetClient' import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' -export async function requestGetPublicKey(dbCom: DbCommunity): Promise { +export async function requestGetPublicKey( + dbCom: DbFederatedCommunity, +): Promise { let endpoint = dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/' endpoint = `${endpoint}${dbCom.apiVersion}/` logger.info(`requestGetPublicKey with endpoint='${endpoint}'...`) diff --git a/backend/src/federation/validateCommunities.ts b/backend/src/federation/validateCommunities.ts index 0e8c7cb12..b38f38ee9 100644 --- a/backend/src/federation/validateCommunities.ts +++ b/backend/src/federation/validateCommunities.ts @@ -1,5 +1,7 @@ +/** eslint-disable @typescript-eslint/no-unsafe-call */ +/** eslint-disable @typescript-eslint/no-unsafe-assignment */ import { IsNull } from '@dbTools/typeorm' -import { Community as DbCommunity } from '@entity/Community' +import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' @@ -23,13 +25,14 @@ export function startValidateCommunities(timerInterval: number): void { } export async function validateCommunities(): Promise { - const dbCommunities: DbCommunity[] = await DbCommunity.createQueryBuilder() - .where({ foreign: true, verifiedAt: IsNull() }) - .orWhere('verified_at < last_announced_at') - .getMany() + const dbFederatedCommunities: DbFederatedCommunity[] = + await DbFederatedCommunity.createQueryBuilder() + .where({ foreign: true, verifiedAt: IsNull() }) + .orWhere('verified_at < last_announced_at') + .getMany() - logger.debug(`Federation: found ${dbCommunities.length} dbCommunities`) - for (const dbCom of dbCommunities) { + logger.debug(`Federation: found ${dbFederatedCommunities.length} dbCommunities`) + for (const dbCom of dbFederatedCommunities) { logger.debug('Federation: dbCom', dbCom) const apiValueStrings: string[] = Object.values(ApiVersionType) logger.debug(`suppported ApiVersions=`, apiValueStrings) @@ -46,7 +49,7 @@ export async function validateCommunities(): Promise { ) if (pubKey && pubKey === dbCom.publicKey.toString()) { logger.info(`Federation: matching publicKey: ${pubKey}`) - await DbCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() }) + await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() }) logger.debug(`Federation: updated dbCom: ${JSON.stringify(dbCom)}`) } else { logger.warn( @@ -74,7 +77,9 @@ function isLogError(err: unknown) { return err instanceof LogError } -async function invokeVersionedRequestGetPublicKey(dbCom: DbCommunity): Promise { +async function invokeVersionedRequestGetPublicKey( + dbCom: DbFederatedCommunity, +): Promise { switch (dbCom.apiVersion) { case ApiVersionType.V1_0: return v1_0_requestGetPublicKey(dbCom) diff --git a/backend/src/graphql/model/Community.ts b/backend/src/graphql/model/Community.ts index d79d40034..bc310a39f 100644 --- a/backend/src/graphql/model/Community.ts +++ b/backend/src/graphql/model/Community.ts @@ -6,14 +6,12 @@ export class Community { constructor(dbCom: DbCommunity) { this.id = dbCom.id this.foreign = dbCom.foreign - this.publicKey = dbCom.publicKey.toString() - this.url = - (dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/') + dbCom.apiVersion - this.lastAnnouncedAt = dbCom.lastAnnouncedAt - this.verifiedAt = dbCom.verifiedAt - this.lastErrorAt = dbCom.lastErrorAt - this.createdAt = dbCom.createdAt - this.updatedAt = dbCom.updatedAt + this.name = dbCom.name + this.description = dbCom.description + this.url = dbCom.url + this.creationDate = dbCom.creationDate + this.uuid = dbCom.communityUuid + this.authenticatedAt = dbCom.authenticatedAt } @Field(() => Int) @@ -22,24 +20,21 @@ export class Community { @Field(() => Boolean) foreign: boolean - @Field(() => String) - publicKey: string + @Field(() => String, { nullable: true }) + name: string | null + + @Field(() => String, { nullable: true }) + description: string | null @Field(() => String) url: string @Field(() => Date, { nullable: true }) - lastAnnouncedAt: Date | null + creationDate: Date | null + + @Field(() => String, { nullable: true }) + uuid: string | null @Field(() => Date, { nullable: true }) - verifiedAt: Date | null - - @Field(() => Date, { nullable: true }) - lastErrorAt: Date | null - - @Field(() => Date, { nullable: true }) - createdAt: Date | null - - @Field(() => Date, { nullable: true }) - updatedAt: Date | null + authenticatedAt: Date | null } diff --git a/backend/src/graphql/model/FederatedCommunity.ts b/backend/src/graphql/model/FederatedCommunity.ts new file mode 100644 index 000000000..856a10d23 --- /dev/null +++ b/backend/src/graphql/model/FederatedCommunity.ts @@ -0,0 +1,45 @@ +import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' +import { ObjectType, Field, Int } from 'type-graphql' + +@ObjectType() +export class FederatedCommunity { + constructor(dbCom: DbFederatedCommunity) { + this.id = dbCom.id + this.foreign = dbCom.foreign + this.publicKey = dbCom.publicKey.toString() + this.url = + (dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/') + dbCom.apiVersion + this.lastAnnouncedAt = dbCom.lastAnnouncedAt + this.verifiedAt = dbCom.verifiedAt + this.lastErrorAt = dbCom.lastErrorAt + this.createdAt = dbCom.createdAt + this.updatedAt = dbCom.updatedAt + } + + @Field(() => Int) + id: number + + @Field(() => Boolean) + foreign: boolean + + @Field(() => String) + publicKey: string + + @Field(() => String) + url: string + + @Field(() => Date, { nullable: true }) + lastAnnouncedAt: Date | null + + @Field(() => Date, { nullable: true }) + verifiedAt: Date | null + + @Field(() => Date, { nullable: true }) + lastErrorAt: Date | null + + @Field(() => Date, { nullable: true }) + createdAt: Date | null + + @Field(() => Date, { nullable: true }) + updatedAt: Date | null +} diff --git a/backend/src/graphql/resolver/CommunityResolver.test.ts b/backend/src/graphql/resolver/CommunityResolver.test.ts index 26f4a3390..0e08896c0 100644 --- a/backend/src/graphql/resolver/CommunityResolver.test.ts +++ b/backend/src/graphql/resolver/CommunityResolver.test.ts @@ -5,7 +5,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { Community as DbCommunity } from '@entity/Community' +import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { testEnvironment } from '@test/helpers' @@ -21,7 +21,7 @@ beforeAll(async () => { testEnv = await testEnvironment() query = testEnv.query con = testEnv.con - await DbCommunity.clear() + await DbFederatedCommunity.clear() }) afterAll(async () => { @@ -30,12 +30,12 @@ afterAll(async () => { describe('CommunityResolver', () => { describe('getCommunities', () => { - let homeCom1: DbCommunity - let homeCom2: DbCommunity - let homeCom3: DbCommunity - let foreignCom1: DbCommunity - let foreignCom2: DbCommunity - let foreignCom3: DbCommunity + let homeCom1: DbFederatedCommunity + let homeCom2: DbFederatedCommunity + let homeCom3: DbFederatedCommunity + let foreignCom1: DbFederatedCommunity + let foreignCom2: DbFederatedCommunity + let foreignCom3: DbFederatedCommunity describe('with empty list', () => { it('returns no community entry', async () => { @@ -53,29 +53,29 @@ describe('CommunityResolver', () => { beforeEach(async () => { jest.clearAllMocks() - homeCom1 = DbCommunity.create() + homeCom1 = DbFederatedCommunity.create() homeCom1.foreign = false homeCom1.publicKey = Buffer.from('publicKey-HomeCommunity') homeCom1.apiVersion = '1_0' homeCom1.endPoint = 'http://localhost/api' homeCom1.createdAt = new Date() - await DbCommunity.insert(homeCom1) + await DbFederatedCommunity.insert(homeCom1) - homeCom2 = DbCommunity.create() + homeCom2 = DbFederatedCommunity.create() homeCom2.foreign = false homeCom2.publicKey = Buffer.from('publicKey-HomeCommunity') homeCom2.apiVersion = '1_1' homeCom2.endPoint = 'http://localhost/api' homeCom2.createdAt = new Date() - await DbCommunity.insert(homeCom2) + await DbFederatedCommunity.insert(homeCom2) - homeCom3 = DbCommunity.create() + homeCom3 = DbFederatedCommunity.create() homeCom3.foreign = false homeCom3.publicKey = Buffer.from('publicKey-HomeCommunity') homeCom3.apiVersion = '2_0' homeCom3.endPoint = 'http://localhost/api' homeCom3.createdAt = new Date() - await DbCommunity.insert(homeCom3) + await DbFederatedCommunity.insert(homeCom3) }) it('returns 3 home-community entries', async () => { @@ -125,29 +125,29 @@ describe('CommunityResolver', () => { beforeEach(async () => { jest.clearAllMocks() - foreignCom1 = DbCommunity.create() + foreignCom1 = DbFederatedCommunity.create() foreignCom1.foreign = true foreignCom1.publicKey = Buffer.from('publicKey-ForeignCommunity') foreignCom1.apiVersion = '1_0' foreignCom1.endPoint = 'http://remotehost/api' foreignCom1.createdAt = new Date() - await DbCommunity.insert(foreignCom1) + await DbFederatedCommunity.insert(foreignCom1) - foreignCom2 = DbCommunity.create() + foreignCom2 = DbFederatedCommunity.create() foreignCom2.foreign = true foreignCom2.publicKey = Buffer.from('publicKey-ForeignCommunity') foreignCom2.apiVersion = '1_1' foreignCom2.endPoint = 'http://remotehost/api' foreignCom2.createdAt = new Date() - await DbCommunity.insert(foreignCom2) + await DbFederatedCommunity.insert(foreignCom2) - foreignCom3 = DbCommunity.create() + foreignCom3 = DbFederatedCommunity.create() foreignCom3.foreign = true foreignCom3.publicKey = Buffer.from('publicKey-ForeignCommunity') foreignCom3.apiVersion = '1_2' foreignCom3.endPoint = 'http://remotehost/api' foreignCom3.createdAt = new Date() - await DbCommunity.insert(foreignCom3) + await DbFederatedCommunity.insert(foreignCom3) }) it('returns 3 home community and 3 foreign community entries', async () => { diff --git a/backend/src/graphql/resolver/CommunityResolver.ts b/backend/src/graphql/resolver/CommunityResolver.ts index 19a499a9b..7306fdffb 100644 --- a/backend/src/graphql/resolver/CommunityResolver.ts +++ b/backend/src/graphql/resolver/CommunityResolver.ts @@ -1,22 +1,38 @@ import { Community as DbCommunity } from '@entity/Community' +import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' +import { Community } from '@model/Community' import { Resolver, Query, Authorized } from 'type-graphql' -import { Community } from '@model/Community' +import { FederatedCommunity } from '@model/FederatedCommunity' + import { RIGHTS } from '@/auth/RIGHTS' @Resolver() export class CommunityResolver { @Authorized([RIGHTS.COMMUNITIES]) - @Query(() => [Community]) - async getCommunities(): Promise { - const dbCommunities: DbCommunity[] = await DbCommunity.find({ + @Query(() => [FederatedCommunity]) + async getCommunities(): Promise { + const dbFederatedCommunities: DbFederatedCommunity[] = await DbFederatedCommunity.find({ order: { foreign: 'ASC', createdAt: 'DESC', lastAnnouncedAt: 'DESC', }, }) + return dbFederatedCommunities.map( + (dbCom: DbFederatedCommunity) => new FederatedCommunity(dbCom), + ) + } + + @Authorized([RIGHTS.COMMUNITIES]) + @Query(() => [Community]) + async getCommunitySelections(): Promise { + const dbCommunities: DbCommunity[] = await DbCommunity.find({ + order: { + name: 'ASC', + }, + }) return dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom)) } } diff --git a/database/entity/0065-refactor_communities_table/Community.ts b/database/entity/0065-refactor_communities_table/Community.ts new file mode 100644 index 000000000..5857634a6 --- /dev/null +++ b/database/entity/0065-refactor_communities_table/Community.ts @@ -0,0 +1,60 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm' + +@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: 64, nullable: false }) + publicKey: Buffer + + @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 +} diff --git a/database/entity/0065-refactor_communities_table/FederatedCommunity.ts b/database/entity/0065-refactor_communities_table/FederatedCommunity.ts new file mode 100644 index 000000000..0adbf4612 --- /dev/null +++ b/database/entity/0065-refactor_communities_table/FederatedCommunity.ts @@ -0,0 +1,51 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm' + +@Entity('federated_communities') +export class FederatedCommunity extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ name: 'foreign', type: 'bool', nullable: false, default: true }) + foreign: boolean + + @Column({ name: 'public_key', type: 'binary', length: 64, default: null, nullable: true }) + publicKey: Buffer + + @Column({ name: 'api_version', length: 10, nullable: false }) + apiVersion: string + + @Column({ name: 'end_point', length: 255, nullable: false }) + endPoint: string + + @Column({ name: 'last_announced_at', type: 'datetime', nullable: true }) + lastAnnouncedAt: Date | null + + @Column({ name: 'verified_at', type: 'datetime', nullable: true }) + verifiedAt: Date | null + + @Column({ name: 'last_error_at', type: 'datetime', nullable: true }) + lastErrorAt: Date | null + + @CreateDateColumn({ + name: 'created_at', + type: 'datetime', + default: () => 'CURRENT_TIMESTAMP(3)', + nullable: false, + }) + createdAt: Date + + @UpdateDateColumn({ + name: 'updated_at', + type: 'datetime', + onUpdate: 'CURRENT_TIMESTAMP(3)', + nullable: true, + }) + updatedAt: Date | null +} diff --git a/database/entity/Community.ts b/database/entity/Community.ts index 80e5ace30..ee08323b6 100644 --- a/database/entity/Community.ts +++ b/database/entity/Community.ts @@ -1 +1 @@ -export { Community } from './0060-update_communities_table/Community' +export { Community } from './0065-refactor_communities_table/Community' diff --git a/database/entity/FederatedCommunity.ts b/database/entity/FederatedCommunity.ts new file mode 100644 index 000000000..cacaaff9c --- /dev/null +++ b/database/entity/FederatedCommunity.ts @@ -0,0 +1 @@ +export { FederatedCommunity } from './0065-refactor_communities_table/FederatedCommunity' diff --git a/database/entity/index.ts b/database/entity/index.ts index 2d9d04c3b..d44029754 100644 --- a/database/entity/index.ts +++ b/database/entity/index.ts @@ -10,6 +10,7 @@ import { Contribution } from './Contribution' import { Event } from './Event' import { ContributionMessage } from './ContributionMessage' import { Community } from './Community' +import { FederatedCommunity } from './FederatedCommunity' export const entities = [ Community, @@ -17,6 +18,7 @@ export const entities = [ ContributionLink, ContributionMessage, Event, + FederatedCommunity, LoginElopageBuys, LoginEmailOptIn, Migration, diff --git a/database/migrations/0065-refactor_communities_table.ts b/database/migrations/0065-refactor_communities_table.ts new file mode 100644 index 000000000..6cba8a174 --- /dev/null +++ b/database/migrations/0065-refactor_communities_table.ts @@ -0,0 +1,37 @@ +/* MIGRATION TO CREATE THE FEDERATION COMMUNITY TABLES + * + * This migration creates the `community` and 'communityfederation' tables in the `apollo` database (`gradido_community`). + */ + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn(`RENAME TABLE communities TO federated_communities;`) + await queryFn(` + CREATE TABLE communities ( + \`id\` int unsigned NOT NULL AUTO_INCREMENT, + \`foreign\` tinyint(4) NOT NULL DEFAULT 1, + \`url\` varchar(255) NOT NULL, + \`public_key\` binary(64) NOT NULL, + \`community_uuid\` char(36) NULL, + \`authenticated_at\` datetime(3) NULL, + \`name\` varchar(40) NULL, + \`description\` varchar(255) NULL, + \`creation_date\` datetime(3) NULL, + \`created_at\` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + \`updated_at\` datetime(3), + PRIMARY KEY (id), + UNIQUE KEY url_key (url), + UNIQUE KEY uuid_key (community_uuid), + UNIQUE KEY public_key_key (public_key) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + `) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + // write downgrade logic as parameter of queryFn + await queryFn(`DROP TABLE communities;`) + await queryFn(`RENAME TABLE federated_communities TO communities;`) + +} diff --git a/dht-node/src/config/index.ts b/dht-node/src/config/index.ts index 078c0fbf3..5c4676337 100644 --- a/dht-node/src/config/index.ts +++ b/dht-node/src/config/index.ts @@ -3,7 +3,7 @@ import dotenv from 'dotenv' dotenv.config() const constants = { - DB_VERSION: '0064-event_rename', + DB_VERSION: '0065-refactor_communities_table', LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info LOG_LEVEL: process.env.LOG_LEVEL || 'info', diff --git a/dht-node/src/dht_node/index.test.ts b/dht-node/src/dht_node/index.test.ts index ac5b1b21a..e76e6ac9f 100644 --- a/dht-node/src/dht_node/index.test.ts +++ b/dht-node/src/dht_node/index.test.ts @@ -5,7 +5,7 @@ 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' CONFIG.FEDERATION_DHT_SEED = '64ebcb0e3ad547848fef4197c6e2332f' @@ -261,7 +261,7 @@ describe('federation', () => { describe('with receiving wrong but tolerated property data', () => { let jsonArray: any[] - let result: DbCommunity[] = [] + let result: DbFederatedCommunity[] = [] beforeAll(async () => { jest.clearAllMocks() jsonArray = [ @@ -277,7 +277,7 @@ describe('federation', () => { }, ] await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) - result = await DbCommunity.find({ foreign: true }) + result = await DbFederatedCommunity.find({ foreign: true }) }) afterAll(async () => { @@ -523,7 +523,7 @@ describe('federation', () => { describe('with receiving data of exact max allowed properties length', () => { let jsonArray: any[] - let result: DbCommunity[] = [] + let result: DbFederatedCommunity[] = [] beforeAll(async () => { jest.clearAllMocks() jsonArray = [ @@ -538,7 +538,7 @@ describe('federation', () => { { api: 'toolong api', url: 'some valid url' }, ] await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) - result = await DbCommunity.find({ foreign: true }) + result = await DbFederatedCommunity.find({ foreign: true }) }) afterAll(async () => { @@ -570,7 +570,7 @@ describe('federation', () => { describe('with receiving data of exact max allowed buffer length', () => { let jsonArray: any[] - let result: DbCommunity[] = [] + let result: DbFederatedCommunity[] = [] beforeAll(async () => { jest.clearAllMocks() jsonArray = [ @@ -592,7 +592,7 @@ describe('federation', () => { }, ] await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) - result = await DbCommunity.find({ foreign: true }) + result = await DbFederatedCommunity.find({ foreign: true }) }) afterAll(async () => { @@ -711,7 +711,7 @@ describe('federation', () => { }) describe('with proper data', () => { - let result: DbCommunity[] = [] + let result: DbFederatedCommunity[] = [] beforeAll(async () => { jest.clearAllMocks() await socketEventMocks.data( @@ -728,7 +728,7 @@ describe('federation', () => { ]), ), ) - result = await DbCommunity.find({ foreign: true }) + result = await DbFederatedCommunity.find({ foreign: true }) }) afterAll(async () => { diff --git a/dht-node/src/dht_node/index.ts b/dht-node/src/dht_node/index.ts index d101037ae..0db7a28c2 100644 --- a/dht-node/src/dht_node/index.ts +++ b/dht-node/src/dht_node/index.ts @@ -3,7 +3,7 @@ import DHT from '@hyperswarm/dht' import { logger } from '@/server/logger' import CONFIG from '@/config' -import { Community as DbCommunity } from '@entity/Community' +import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' const KEY_SECRET_SEEDBYTES = 32 const getSeed = (): Buffer | null => @@ -31,7 +31,7 @@ export const startDHT = async (topic: string): Promise => { logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`) logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`) - const ownApiVersions = await writeHomeCommunityEnries(keyPair.publicKey) + const ownApiVersions = await writeFederatedHomeCommunityEnries(keyPair.publicKey) logger.info(`ApiList: ${JSON.stringify(ownApiVersions)}`) const node = new DHT({ keyPair }) @@ -92,9 +92,9 @@ export const startDHT = async (topic: string): Promise => { } logger.debug(`upsert with variables=${JSON.stringify(variables)}`) // this will NOT update the updatedAt column, to distingue between a normal update and the last announcement - await DbCommunity.createQueryBuilder() + await DbFederatedCommunity.createQueryBuilder() .insert() - .into(DbCommunity) + .into(DbFederatedCommunity) .values(variables) .orUpdate({ conflict_target: ['id', 'publicKey', 'apiVersion'], @@ -179,7 +179,7 @@ export const startDHT = async (topic: string): Promise => { } } -async function writeHomeCommunityEnries(pubKey: any): Promise { +async function writeFederatedHomeCommunityEnries(pubKey: any): Promise { const homeApiVersions: CommunityApi[] = Object.values(ApiVersionType).map(function (apiEnum) { const comApi: CommunityApi = { api: apiEnum, @@ -189,17 +189,17 @@ async function writeHomeCommunityEnries(pubKey: any): Promise { }) try { // first remove privious existing homeCommunity entries - DbCommunity.createQueryBuilder().delete().where({ foreign: false }).execute() + DbFederatedCommunity.createQueryBuilder().delete().where({ foreign: false }).execute() homeApiVersions.forEach(async function (homeApi) { - const homeCom = new DbCommunity() + const homeCom = new DbFederatedCommunity() homeCom.foreign = false homeCom.apiVersion = homeApi.api homeCom.endPoint = homeApi.url homeCom.publicKey = pubKey.toString('hex') // this will NOT update the updatedAt column, to distingue between a normal update and the last announcement - await DbCommunity.insert(homeCom) + await DbFederatedCommunity.insert(homeCom) logger.info(`federation home-community inserted successfully: ${JSON.stringify(homeCom)}`) }) } catch (err) { diff --git a/federation/src/config/index.ts b/federation/src/config/index.ts index ce0c5a9a5..70a155d63 100644 --- a/federation/src/config/index.ts +++ b/federation/src/config/index.ts @@ -11,7 +11,7 @@ Decimal.set({ */ const constants = { - DB_VERSION: '0064-event_rename', + DB_VERSION: '0065-refactor_communities_table', // 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 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 20e6c8228..18d2a7599 100644 --- a/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.test.ts +++ b/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.test.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { createTestClient } from 'apollo-server-testing' import createServer from '@/server/createServer' -import { Community as DbCommunity } from '@entity/Community' +import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' let query: any @@ -13,7 +13,7 @@ beforeAll(async () => { const server = await createServer() con = server.con query = createTestClient(server.apollo).query - DbCommunity.clear() + DbFederatedCommunity.clear() }) afterAll(async () => { @@ -32,12 +32,12 @@ describe('PublicKeyResolver', () => { describe('getPublicKey', () => { beforeEach(async () => { - const homeCom = new DbCommunity() + const homeCom = new DbFederatedCommunity() homeCom.foreign = false homeCom.apiVersion = '1_0' homeCom.endPoint = 'endpoint-url' homeCom.publicKey = Buffer.from('homeCommunity-publicKey') - await DbCommunity.insert(homeCom) + await DbFederatedCommunity.insert(homeCom) }) it('returns homeCommunity-publicKey', async () => { diff --git a/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.ts b/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.ts index f96f20c58..0145324fc 100644 --- a/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/PublicKeyResolver.ts @@ -1,7 +1,7 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars import { Query, Resolver } from 'type-graphql' import { federationLogger as logger } from '@/server/logger' -import { Community as DbCommunity } from '@entity/Community' +import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { GetPublicKeyResult } from '../model/GetPublicKeyResult' @Resolver() @@ -10,7 +10,7 @@ export class PublicKeyResolver { @Query(() => GetPublicKeyResult) async getPublicKey(): Promise { logger.debug(`getPublicKey() via apiVersion=1_0 ...`) - const homeCom = await DbCommunity.findOneOrFail({ + const homeCom = await DbFederatedCommunity.findOneOrFail({ foreign: false, apiVersion: '1_0', }) From 3fdeec0783a8e1c252b3aa2d5cd6900013a28aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Thu, 27 Apr 2023 21:35:18 +0200 Subject: [PATCH 058/153] linting --- backend/src/graphql/model/Community.ts | 2 +- backend/src/graphql/resolver/CommunityResolver.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/src/graphql/model/Community.ts b/backend/src/graphql/model/Community.ts index bc310a39f..43e0a7108 100644 --- a/backend/src/graphql/model/Community.ts +++ b/backend/src/graphql/model/Community.ts @@ -30,7 +30,7 @@ export class Community { url: string @Field(() => Date, { nullable: true }) - creationDate: Date | null + creationDate: Date | null @Field(() => String, { nullable: true }) uuid: string | null diff --git a/backend/src/graphql/resolver/CommunityResolver.ts b/backend/src/graphql/resolver/CommunityResolver.ts index 7306fdffb..4c6c8e785 100644 --- a/backend/src/graphql/resolver/CommunityResolver.ts +++ b/backend/src/graphql/resolver/CommunityResolver.ts @@ -1,11 +1,10 @@ import { Community as DbCommunity } from '@entity/Community' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' -import { Community } from '@model/Community' import { Resolver, Query, Authorized } from 'type-graphql' +import { Community } from '@model/Community' import { FederatedCommunity } from '@model/FederatedCommunity' - import { RIGHTS } from '@/auth/RIGHTS' @Resolver() From f9f01f94ea11fc027102057b5e1d71347815d9f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Thu, 27 Apr 2023 21:44:42 +0200 Subject: [PATCH 059/153] linting --- database/migrations/0065-refactor_communities_table.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/database/migrations/0065-refactor_communities_table.ts b/database/migrations/0065-refactor_communities_table.ts index 6cba8a174..06f5b3990 100644 --- a/database/migrations/0065-refactor_communities_table.ts +++ b/database/migrations/0065-refactor_communities_table.ts @@ -33,5 +33,4 @@ export async function downgrade(queryFn: (query: string, values?: any[]) => Prom // write downgrade logic as parameter of queryFn await queryFn(`DROP TABLE communities;`) await queryFn(`RENAME TABLE federated_communities TO communities;`) - } From d562371cbc5bddfd26ef0882bd372634ccb3994e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Thu, 27 Apr 2023 22:10:53 +0200 Subject: [PATCH 060/153] use correct renamed entity in tests too --- .../src/federation/validateCommunities.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/src/federation/validateCommunities.test.ts b/backend/src/federation/validateCommunities.test.ts index d90664b63..10df70489 100644 --- a/backend/src/federation/validateCommunities.test.ts +++ b/backend/src/federation/validateCommunities.test.ts @@ -5,7 +5,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { Community as DbCommunity } from '@entity/Community' +import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { testEnvironment, cleanDB } from '@test/helpers' import { logger } from '@test/testSetup' @@ -59,9 +59,9 @@ describe('validate Communities', () => { endPoint: 'http//localhost:5001/api/', lastAnnouncedAt: new Date(), } - await DbCommunity.createQueryBuilder() + await DbFederatedCommunity.createQueryBuilder() .insert() - .into(DbCommunity) + .into(DbFederatedCommunity) .values(variables1) .orUpdate({ conflict_target: ['id', 'publicKey', 'apiVersion'], @@ -90,9 +90,9 @@ describe('validate Communities', () => { endPoint: 'http//localhost:5001/api/', lastAnnouncedAt: new Date(), } - await DbCommunity.createQueryBuilder() + await DbFederatedCommunity.createQueryBuilder() .insert() - .into(DbCommunity) + .into(DbFederatedCommunity) .values(variables2) .orUpdate({ conflict_target: ['id', 'publicKey', 'apiVersion'], @@ -118,7 +118,7 @@ describe('validate Communities', () => { }) }) describe('with three Communities of api 1_0, 1_1 and 2_0', () => { - let dbCom: DbCommunity + let dbCom: DbFederatedCommunity beforeEach(async () => { const variables3 = { publicKey: Buffer.from('11111111111111111111111111111111'), @@ -126,16 +126,16 @@ describe('validate Communities', () => { endPoint: 'http//localhost:5001/api/', lastAnnouncedAt: new Date(), } - await DbCommunity.createQueryBuilder() + await DbFederatedCommunity.createQueryBuilder() .insert() - .into(DbCommunity) + .into(DbFederatedCommunity) .values(variables3) .orUpdate({ conflict_target: ['id', 'publicKey', 'apiVersion'], overwrite: ['end_point', 'last_announced_at'], }) .execute() - dbCom = await DbCommunity.findOneOrFail({ + dbCom = await DbFederatedCommunity.findOneOrFail({ where: { publicKey: variables3.publicKey, apiVersion: variables3.apiVersion }, }) jest.clearAllMocks() From 9b9c0ea8cbc578bd15d802e1ec1ae1490891be39 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 3 May 2023 12:37:29 +0200 Subject: [PATCH 061/153] merge conflict --- backend/src/graphql/directive/isAuthorized.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/directive/isAuthorized.ts b/backend/src/graphql/directive/isAuthorized.ts index 504e8d27b..b8595a2bd 100644 --- a/backend/src/graphql/directive/isAuthorized.ts +++ b/backend/src/graphql/directive/isAuthorized.ts @@ -8,7 +8,7 @@ import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN } from '@/auth/ROLES' import { Context } from '@/server/context' import { LogError } from '@/server/LogError' -const isAuthorized: AuthChecker = async ({ context }, rights) => { +export const isAuthorized: AuthChecker = async ({ context }, rights) => { context.role = ROLE_UNAUTHORIZED // unauthorized user // is rights an inalienable right? From 870c97ce71231febd4ab4781eb51c8b6271a6f90 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 3 May 2023 13:02:32 +0200 Subject: [PATCH 062/153] rfactor federation clients --- .../federation/client/1_0/FederationClient.ts | 42 -------------- .../federation/client/1_1/FederationClient.ts | 42 -------------- backend/src/federation/client/Client.ts | 57 +++++++++++++++++++ backend/src/federation/client/Client_1_0.ts | 54 ++++++++++++++++++ backend/src/federation/client/Client_1_1.ts | 5 ++ .../src/federation/client/GraphQLGetClient.ts | 43 -------------- backend/src/federation/validateCommunities.ts | 18 +----- 7 files changed, 118 insertions(+), 143 deletions(-) delete mode 100644 backend/src/federation/client/1_0/FederationClient.ts delete mode 100644 backend/src/federation/client/1_1/FederationClient.ts create mode 100644 backend/src/federation/client/Client.ts create mode 100644 backend/src/federation/client/Client_1_0.ts create mode 100644 backend/src/federation/client/Client_1_1.ts delete mode 100644 backend/src/federation/client/GraphQLGetClient.ts diff --git a/backend/src/federation/client/1_0/FederationClient.ts b/backend/src/federation/client/1_0/FederationClient.ts deleted file mode 100644 index 13f05e761..000000000 --- a/backend/src/federation/client/1_0/FederationClient.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { Community as DbCommunity } from '@entity/Community' -import { gql } from 'graphql-request' - -import { GraphQLGetClient } from '@/federation/client/GraphQLGetClient' -import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' - -export async function requestGetPublicKey(dbCom: DbCommunity): Promise { - let endpoint = dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/' - endpoint = `${endpoint}${dbCom.apiVersion}/` - logger.info(`requestGetPublicKey with endpoint='${endpoint}'...`) - - const graphQLClient = GraphQLGetClient.getInstance(endpoint) - logger.debug(`graphQLClient=${JSON.stringify(graphQLClient)}`) - const query = gql` - query { - getPublicKey { - publicKey - } - } - ` - const variables = {} - - try { - const { data, errors, extensions, headers, status } = await graphQLClient.rawRequest( - query, - variables, - ) - logger.debug(`Response-Data:`, data, errors, extensions, headers, status) - if (data) { - logger.debug(`Response-PublicKey:`, data.getPublicKey.publicKey) - logger.info(`requestGetPublicKey processed successfully`) - return data.getPublicKey.publicKey - } - logger.warn(`requestGetPublicKey processed without response data`) - } catch (err) { - throw new LogError(`Request-Error:`, err) - } -} diff --git a/backend/src/federation/client/1_1/FederationClient.ts b/backend/src/federation/client/1_1/FederationClient.ts deleted file mode 100644 index bda185fba..000000000 --- a/backend/src/federation/client/1_1/FederationClient.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import { Community as DbCommunity } from '@entity/Community' -import { gql } from 'graphql-request' - -import { GraphQLGetClient } from '@/federation/client/GraphQLGetClient' -import { LogError } from '@/server/LogError' -import { backendLogger as logger } from '@/server/logger' - -export async function requestGetPublicKey(dbCom: DbCommunity): Promise { - let endpoint = dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/' - endpoint = `${endpoint}${dbCom.apiVersion}/` - logger.info(`requestGetPublicKey with endpoint='${endpoint}'...`) - - const graphQLClient = GraphQLGetClient.getInstance(endpoint) - logger.debug(`graphQLClient=${JSON.stringify(graphQLClient)}`) - const query = gql` - query { - getPublicKey { - publicKey - } - } - ` - const variables = {} - - try { - const { data, errors, extensions, headers, status } = await graphQLClient.rawRequest( - query, - variables, - ) - logger.debug(`Response-Data:`, data, errors, extensions, headers, status) - if (data) { - logger.debug(`Response-PublicKey:`, data.getPublicKey.publicKey) - logger.info(`requestGetPublicKey processed successfully`) - return data.getPublicKey.publicKey - } - logger.warn(`requestGetPublicKey processed without response data`) - } catch (err) { - throw new LogError(`Request-Error:`, err) - } -} diff --git a/backend/src/federation/client/Client.ts b/backend/src/federation/client/Client.ts new file mode 100644 index 000000000..e285b591c --- /dev/null +++ b/backend/src/federation/client/Client.ts @@ -0,0 +1,57 @@ +import { Community as DbCommunity } from '@entity/Community' + +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' + +// eslint-disable-next-line camelcase +type FederationClient = Client_1_0 | Client_1_1 + +type ClientInstance = { + dbCom: DbCommunity + // eslint-disable-next-line no-use-before-define + client: FederationClient +} + +export class Client { + private static instanceArray: ClientInstance[] = [] + + /** + * 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() {} + + private static createFederationClient = (dbCom: DbCommunity) => { + switch (dbCom.apiVersion) { + case ApiVersionType.V1_0: + return new Client_1_0(dbCom) + case ApiVersionType.V1_1: + return new Client_1_1(dbCom) + default: + return null + } + } + + /** + * The static method that controls the access to the singleton instance. + * + * This implementation let you subclass the Singleton class while keeping + * just one instance of each subclass around. + */ + public static getInstance(dbCom: DbCommunity): FederationClient | null { + const instance = Client.instanceArray.find((instance) => instance.dbCom === dbCom) + if (instance) { + return instance.client + } + const client = Client.createFederationClient(dbCom) + if (client) { + Client.instanceArray.push({ dbCom, client } as ClientInstance) + } + return client + } +} diff --git a/backend/src/federation/client/Client_1_0.ts b/backend/src/federation/client/Client_1_0.ts new file mode 100644 index 000000000..6adfaa48c --- /dev/null +++ b/backend/src/federation/client/Client_1_0.ts @@ -0,0 +1,54 @@ +import { Community as DbCommunity } from '@entity/Community' +import { GraphQLClient, gql } from 'graphql-request' + +import { LogError } from '@/server/LogError' +import { backendLogger as logger } from '@/server/logger' + +// eslint-disable-next-line camelcase +export class Client_1_0 { + dbCom: DbCommunity + endpoint: string + client: GraphQLClient + + constructor(dbCom: DbCommunity) { + this.dbCom = dbCom + this.endpoint = `${dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'}${ + dbCom.apiVersion + }/` + this.client = new GraphQLClient(this.endpoint, { + method: 'GET', + jsonSerializer: { + parse: JSON.parse, + stringify: JSON.stringify, + }, + }) + } + + requestGetPublicKey = async (): Promise => { + logger.info(`requestGetPublicKey with endpoint='${this.endpoint}'...`) + + const query = gql` + query { + getPublicKey { + publicKey + } + } + ` + + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { data, errors, headers, status } = await this.client.rawRequest(query) + logger.debug(`Response-Data:`, data, errors, headers, status) + if (data) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + logger.debug(`Response-PublicKey:`, data.getPublicKey.publicKey) + logger.info(`requestGetPublicKey processed successfully`) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access + return data.getPublicKey.publicKey + } + logger.warn(`requestGetPublicKey processed without response data`) + } catch (err) { + throw new LogError(`Request-Error:`, err) + } + } +} diff --git a/backend/src/federation/client/Client_1_1.ts b/backend/src/federation/client/Client_1_1.ts new file mode 100644 index 000000000..8525acc5d --- /dev/null +++ b/backend/src/federation/client/Client_1_1.ts @@ -0,0 +1,5 @@ +// 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/GraphQLGetClient.ts b/backend/src/federation/client/GraphQLGetClient.ts deleted file mode 100644 index 2f5281532..000000000 --- a/backend/src/federation/client/GraphQLGetClient.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { GraphQLClient } from 'graphql-request' -import { PatchedRequestInit } from 'graphql-request/dist/types' - -type ClientInstance = { - url: string - // eslint-disable-next-line no-use-before-define - client: GraphQLGetClient -} - -export class GraphQLGetClient extends GraphQLClient { - private static instanceArray: ClientInstance[] = [] - - /** - * The Singleton's constructor should always be private to prevent direct - * construction calls with the `new` operator. - */ - // eslint-disable-next-line no-useless-constructor - private constructor(url: string, options?: PatchedRequestInit) { - super(url, options) - } - - /** - * The static method that controls the access to the singleton instance. - * - * This implementation let you subclass the Singleton class while keeping - * just one instance of each subclass around. - */ - public static getInstance(url: string): GraphQLGetClient { - const instance = GraphQLGetClient.instanceArray.find((instance) => instance.url === url) - if (instance) { - return instance.client - } - const client = new GraphQLGetClient(url, { - method: 'GET', - jsonSerializer: { - parse: JSON.parse, - stringify: JSON.stringify, - }, - }) - GraphQLGetClient.instanceArray.push({ url, client } as ClientInstance) - return client - } -} diff --git a/backend/src/federation/validateCommunities.ts b/backend/src/federation/validateCommunities.ts index 0e8c7cb12..11406199d 100644 --- a/backend/src/federation/validateCommunities.ts +++ b/backend/src/federation/validateCommunities.ts @@ -4,10 +4,7 @@ import { Community as DbCommunity } from '@entity/Community' import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' -// eslint-disable-next-line camelcase -import { requestGetPublicKey as v1_0_requestGetPublicKey } from './client/1_0/FederationClient' -// eslint-disable-next-line camelcase -import { requestGetPublicKey as v1_1_requestGetPublicKey } from './client/1_1/FederationClient' +import { Client } from './client/Client' import { ApiVersionType } from './enum/apiVersionType' export function startValidateCommunities(timerInterval: number): void { @@ -38,7 +35,7 @@ export async function validateCommunities(): Promise { `Federation: validate publicKey for dbCom: ${dbCom.id} with apiVersion=${dbCom.apiVersion}`, ) try { - const pubKey = await invokeVersionedRequestGetPublicKey(dbCom) + const pubKey = await Client.getInstance(dbCom)?.requestGetPublicKey() logger.info( 'Federation: received publicKey from endpoint', pubKey, @@ -73,14 +70,3 @@ export async function validateCommunities(): Promise { function isLogError(err: unknown) { return err instanceof LogError } - -async function invokeVersionedRequestGetPublicKey(dbCom: DbCommunity): Promise { - switch (dbCom.apiVersion) { - case ApiVersionType.V1_0: - return v1_0_requestGetPublicKey(dbCom) - case ApiVersionType.V1_1: - return v1_1_requestGetPublicKey(dbCom) - default: - return undefined - } -} From d3a2df2878d9f2ebdcce8c8018672f08a76ac133 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 3 May 2023 13:03:27 +0200 Subject: [PATCH 063/153] remove obsolete function --- backend/src/federation/validateCommunities.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/backend/src/federation/validateCommunities.ts b/backend/src/federation/validateCommunities.ts index 11406199d..dfd14bdac 100644 --- a/backend/src/federation/validateCommunities.ts +++ b/backend/src/federation/validateCommunities.ts @@ -54,7 +54,7 @@ export async function validateCommunities(): Promise { // DbCommunity.delete({ id: dbCom.id }) } } catch (err) { - if (!isLogError(err)) { + if (!(err instanceof LogError)) { logger.error(`Error:`, err) } } @@ -66,7 +66,3 @@ export async function validateCommunities(): Promise { } } } - -function isLogError(err: unknown) { - return err instanceof LogError -} From 8c76de3ee51e4f52ea27d2da3e651b4b983f1d6f Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 3 May 2023 13:07:54 +0200 Subject: [PATCH 064/153] try fixing auth tests by reverting header token change --- backend/test/helpers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/test/helpers.ts b/backend/test/helpers.ts index 07601227c..d42db959f 100644 --- a/backend/test/helpers.ts +++ b/backend/test/helpers.ts @@ -2,12 +2,12 @@ import { entities } from '@entity/index' import { createTestClient } from 'apollo-server-testing' -import { Context } from '@/server/context' import { createServer } from '@/server/createServer' import { i18n, logger } from './testSetup' -export const headerPushMock = jest.fn(([t]: Context['setHeaders']) => { +export const headerPushMock = jest.fn((t) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access context.token = t.value }) From 4850923f17078a81d31ffee2b35dfda14e706751 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 3 May 2023 13:29:56 +0200 Subject: [PATCH 065/153] variables must be defined --- backend/src/federation/client/Client_1_0.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/federation/client/Client_1_0.ts b/backend/src/federation/client/Client_1_0.ts index 6adfaa48c..efbe497c8 100644 --- a/backend/src/federation/client/Client_1_0.ts +++ b/backend/src/federation/client/Client_1_0.ts @@ -35,9 +35,11 @@ export class Client_1_0 { } ` + const variables = {} + try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { data, errors, headers, status } = await this.client.rawRequest(query) + const { data, errors, headers, status } = await this.client.rawRequest(query, variables) logger.debug(`Response-Data:`, data, errors, headers, status) if (data) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access From 1c6142220f5b86a23fc35cbee2631f9855e07730 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Thu, 4 May 2023 01:40:50 +0200 Subject: [PATCH 066/153] rename getPublicKey function --- backend/src/federation/client/Client_1_0.ts | 2 +- backend/src/federation/validateCommunities.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/federation/client/Client_1_0.ts b/backend/src/federation/client/Client_1_0.ts index efbe497c8..9ce3b6b12 100644 --- a/backend/src/federation/client/Client_1_0.ts +++ b/backend/src/federation/client/Client_1_0.ts @@ -24,7 +24,7 @@ export class Client_1_0 { }) } - requestGetPublicKey = async (): Promise => { + getPublicKey = async (): Promise => { logger.info(`requestGetPublicKey with endpoint='${this.endpoint}'...`) const query = gql` diff --git a/backend/src/federation/validateCommunities.ts b/backend/src/federation/validateCommunities.ts index dfd14bdac..14bf4dd3d 100644 --- a/backend/src/federation/validateCommunities.ts +++ b/backend/src/federation/validateCommunities.ts @@ -35,7 +35,7 @@ export async function validateCommunities(): Promise { `Federation: validate publicKey for dbCom: ${dbCom.id} with apiVersion=${dbCom.apiVersion}`, ) try { - const pubKey = await Client.getInstance(dbCom)?.requestGetPublicKey() + const pubKey = await Client.getInstance(dbCom)?.getPublicKey() logger.info( 'Federation: received publicKey from endpoint', pubKey, From 81552e51ddd50949ffa26efb7470e721a9d93c26 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Thu, 4 May 2023 01:51:36 +0200 Subject: [PATCH 067/153] properly reuse graphql client --- backend/src/federation/client/Client.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/federation/client/Client.ts b/backend/src/federation/client/Client.ts index e285b591c..a33cb4832 100644 --- a/backend/src/federation/client/Client.ts +++ b/backend/src/federation/client/Client.ts @@ -11,7 +11,7 @@ import { Client_1_1 } from './Client_1_1' type FederationClient = Client_1_0 | Client_1_1 type ClientInstance = { - dbCom: DbCommunity + id: number // eslint-disable-next-line no-use-before-define client: FederationClient } @@ -44,13 +44,13 @@ export class Client { * just one instance of each subclass around. */ public static getInstance(dbCom: DbCommunity): FederationClient | null { - const instance = Client.instanceArray.find((instance) => instance.dbCom === dbCom) + const instance = Client.instanceArray.find((instance) => instance.id === dbCom.id) if (instance) { return instance.client } const client = Client.createFederationClient(dbCom) if (client) { - Client.instanceArray.push({ dbCom, client } as ClientInstance) + Client.instanceArray.push({ id: dbCom.id, client } as ClientInstance) } return client } From 1fa33963966fbb0185e5e530242d544ac883410c Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Thu, 4 May 2023 01:55:14 +0200 Subject: [PATCH 068/153] externalize the graphql query --- backend/src/federation/client/Client_1_0.ts | 16 ++++++---------- backend/src/federation/query/getPublicKey.ts | 9 +++++++++ 2 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 backend/src/federation/query/getPublicKey.ts diff --git a/backend/src/federation/client/Client_1_0.ts b/backend/src/federation/client/Client_1_0.ts index 9ce3b6b12..b0fb825ee 100644 --- a/backend/src/federation/client/Client_1_0.ts +++ b/backend/src/federation/client/Client_1_0.ts @@ -1,6 +1,7 @@ import { Community as DbCommunity } from '@entity/Community' -import { GraphQLClient, gql } from 'graphql-request' +import { GraphQLClient } from 'graphql-request' +import { getPublicKey } from '@/federation/query/getPublicKey' import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' @@ -27,19 +28,14 @@ export class Client_1_0 { getPublicKey = async (): Promise => { logger.info(`requestGetPublicKey with endpoint='${this.endpoint}'...`) - const query = gql` - query { - getPublicKey { - publicKey - } - } - ` - const variables = {} try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { data, errors, headers, status } = await this.client.rawRequest(query, variables) + const { data, errors, headers, status } = await this.client.rawRequest( + getPublicKey, + variables, + ) logger.debug(`Response-Data:`, data, errors, headers, status) if (data) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access diff --git a/backend/src/federation/query/getPublicKey.ts b/backend/src/federation/query/getPublicKey.ts new file mode 100644 index 000000000..a772a0cf1 --- /dev/null +++ b/backend/src/federation/query/getPublicKey.ts @@ -0,0 +1,9 @@ +import { gql } from 'graphql-request' + +export const getPublicKey = gql` + query { + getPublicKey { + publicKey + } + } +` From d636f922618b62bc352806a08cbbc1153566a164 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Thu, 4 May 2023 01:57:43 +0200 Subject: [PATCH 069/153] fix problem in federation when variables are not defined --- backend/src/federation/client/Client_1_0.ts | 6 +----- federation/src/server/plugins.ts | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/backend/src/federation/client/Client_1_0.ts b/backend/src/federation/client/Client_1_0.ts index b0fb825ee..df239db92 100644 --- a/backend/src/federation/client/Client_1_0.ts +++ b/backend/src/federation/client/Client_1_0.ts @@ -27,14 +27,10 @@ export class Client_1_0 { getPublicKey = async (): Promise => { logger.info(`requestGetPublicKey with endpoint='${this.endpoint}'...`) - - const variables = {} - try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { data, errors, headers, status } = await this.client.rawRequest( - getPublicKey, - variables, + getPublicKey ) logger.debug(`Response-Data:`, data, errors, headers, status) if (data) { diff --git a/federation/src/server/plugins.ts b/federation/src/server/plugins.ts index 541c68ca2..38fdfbe9f 100644 --- a/federation/src/server/plugins.ts +++ b/federation/src/server/plugins.ts @@ -23,8 +23,8 @@ const setHeadersPlugin = { const filterVariables = (variables: any) => { const vars = clonedeep(variables) - if (vars.password) vars.password = '***' - if (vars.passwordNew) vars.passwordNew = '***' + if (vars && vars.password) vars.password = '***' + if (vars && vars.passwordNew) vars.passwordNew = '***' return vars } From 950ff2f9b680e29e79c5d4a8e415e2e212f3be23 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Thu, 4 May 2023 02:01:45 +0200 Subject: [PATCH 070/153] lint --- backend/src/federation/client/Client_1_0.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/src/federation/client/Client_1_0.ts b/backend/src/federation/client/Client_1_0.ts index df239db92..317306048 100644 --- a/backend/src/federation/client/Client_1_0.ts +++ b/backend/src/federation/client/Client_1_0.ts @@ -29,9 +29,7 @@ export class Client_1_0 { logger.info(`requestGetPublicKey with endpoint='${this.endpoint}'...`) try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { data, errors, headers, status } = await this.client.rawRequest( - getPublicKey - ) + const { data, errors, headers, status } = await this.client.rawRequest(getPublicKey) logger.debug(`Response-Data:`, data, errors, headers, status) if (data) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access From 0a6c29bca3012f5e763cd69bd24fb2b67db64e8e Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Thu, 4 May 2023 11:28:53 +0200 Subject: [PATCH 071/153] variables must be transmitted due to backward compatibility --- backend/src/federation/client/Client_1_0.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/federation/client/Client_1_0.ts b/backend/src/federation/client/Client_1_0.ts index 317306048..d425ccea1 100644 --- a/backend/src/federation/client/Client_1_0.ts +++ b/backend/src/federation/client/Client_1_0.ts @@ -29,7 +29,7 @@ export class Client_1_0 { logger.info(`requestGetPublicKey with endpoint='${this.endpoint}'...`) try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { data, errors, headers, status } = await this.client.rawRequest(getPublicKey) + const { data, errors, headers, status } = await this.client.rawRequest(getPublicKey, {}) logger.debug(`Response-Data:`, data, errors, headers, status) if (data) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access From 59cb0ce4b9477f6cd94cc4f55f8e2d56e8b64edc Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Thu, 4 May 2023 14:59:38 +0200 Subject: [PATCH 072/153] use yarn dev for federation to autoreload on codechange --- federation/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/federation/Dockerfile b/federation/Dockerfile index 959252d29..81b95010e 100644 --- a/federation/Dockerfile +++ b/federation/Dockerfile @@ -86,7 +86,7 @@ RUN cd ../database && yarn run build FROM build as test # Run command -CMD /bin/sh -c "yarn run start" +CMD /bin/sh -c "yarn run dev" ################################################################################## # PRODUCTION (Does contain only "binary"- and static-files to reduce image size) # From 6a63e5956d3641a0bd3ea17cc903a1392a209757 Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Thu, 4 May 2023 20:48:43 +0200 Subject: [PATCH 073/153] adaptions after master-merge --- .../Transaction.ts | 0 database/entity/Transaction.ts | 2 +- ...able.ts => 0066-x-community-sendcoins-transactions_table.ts} | 0 dht-node/src/config/index.ts | 2 +- federation/src/config/index.ts | 2 +- 5 files changed, 3 insertions(+), 3 deletions(-) rename database/entity/{0065-x-community-sendcoins-transactions_table => 0066-x-community-sendcoins-transactions_table}/Transaction.ts (100%) rename database/migrations/{0065-x-community-sendcoins-transactions_table.ts => 0066-x-community-sendcoins-transactions_table.ts} (100%) diff --git a/database/entity/0065-x-community-sendcoins-transactions_table/Transaction.ts b/database/entity/0066-x-community-sendcoins-transactions_table/Transaction.ts similarity index 100% rename from database/entity/0065-x-community-sendcoins-transactions_table/Transaction.ts rename to database/entity/0066-x-community-sendcoins-transactions_table/Transaction.ts diff --git a/database/entity/Transaction.ts b/database/entity/Transaction.ts index 3e3355b13..4000e3c85 100644 --- a/database/entity/Transaction.ts +++ b/database/entity/Transaction.ts @@ -1 +1 @@ -export { Transaction } from './0065-x-community-sendcoins-transactions_table/Transaction' +export { Transaction } from './0066-x-community-sendcoins-transactions_table/Transaction' diff --git a/database/migrations/0065-x-community-sendcoins-transactions_table.ts b/database/migrations/0066-x-community-sendcoins-transactions_table.ts similarity index 100% rename from database/migrations/0065-x-community-sendcoins-transactions_table.ts rename to database/migrations/0066-x-community-sendcoins-transactions_table.ts diff --git a/dht-node/src/config/index.ts b/dht-node/src/config/index.ts index 5c4676337..eca5dbbb5 100644 --- a/dht-node/src/config/index.ts +++ b/dht-node/src/config/index.ts @@ -3,7 +3,7 @@ import dotenv from 'dotenv' dotenv.config() const constants = { - DB_VERSION: '0065-refactor_communities_table', + DB_VERSION: '0066-x-community-sendcoins-transactions_table', LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info LOG_LEVEL: process.env.LOG_LEVEL || 'info', diff --git a/federation/src/config/index.ts b/federation/src/config/index.ts index 70a155d63..66d8a056c 100644 --- a/federation/src/config/index.ts +++ b/federation/src/config/index.ts @@ -11,7 +11,7 @@ Decimal.set({ */ const constants = { - DB_VERSION: '0065-refactor_communities_table', + DB_VERSION: '0066-x-community-sendcoins-transactions_table', // 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 From 2d03ae938ab7d05dfa449b09f0cd376481a08ad5 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Thu, 4 May 2023 22:50:06 +0200 Subject: [PATCH 074/153] merge conflict --- backend/src/federation/client/Client.ts | 6 +++--- backend/src/federation/client/Client_1_0.ts | 6 +++--- backend/src/federation/validateCommunities.ts | 13 ------------- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/backend/src/federation/client/Client.ts b/backend/src/federation/client/Client.ts index a33cb4832..515945eb5 100644 --- a/backend/src/federation/client/Client.ts +++ b/backend/src/federation/client/Client.ts @@ -1,4 +1,4 @@ -import { Community as DbCommunity } from '@entity/Community' +import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { ApiVersionType } from '@/federation/enum/apiVersionType' @@ -26,7 +26,7 @@ export class Client { // eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function private constructor() {} - private static createFederationClient = (dbCom: DbCommunity) => { + private static createFederationClient = (dbCom: DbFederatedCommunity) => { switch (dbCom.apiVersion) { case ApiVersionType.V1_0: return new Client_1_0(dbCom) @@ -43,7 +43,7 @@ export class Client { * This implementation let you subclass the Singleton class while keeping * just one instance of each subclass around. */ - public static getInstance(dbCom: DbCommunity): FederationClient | null { + public static getInstance(dbCom: DbFederatedCommunity): FederationClient | null { const instance = Client.instanceArray.find((instance) => instance.id === dbCom.id) if (instance) { return instance.client diff --git a/backend/src/federation/client/Client_1_0.ts b/backend/src/federation/client/Client_1_0.ts index d425ccea1..456196bcb 100644 --- a/backend/src/federation/client/Client_1_0.ts +++ b/backend/src/federation/client/Client_1_0.ts @@ -1,4 +1,4 @@ -import { Community as DbCommunity } from '@entity/Community' +import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { GraphQLClient } from 'graphql-request' import { getPublicKey } from '@/federation/query/getPublicKey' @@ -7,11 +7,11 @@ import { backendLogger as logger } from '@/server/logger' // eslint-disable-next-line camelcase export class Client_1_0 { - dbCom: DbCommunity + dbCom: DbFederatedCommunity endpoint: string client: GraphQLClient - constructor(dbCom: DbCommunity) { + constructor(dbCom: DbFederatedCommunity) { this.dbCom = dbCom this.endpoint = `${dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'}${ dbCom.apiVersion diff --git a/backend/src/federation/validateCommunities.ts b/backend/src/federation/validateCommunities.ts index 2bc531eaf..6879f98e7 100644 --- a/backend/src/federation/validateCommunities.ts +++ b/backend/src/federation/validateCommunities.ts @@ -69,16 +69,3 @@ export async function validateCommunities(): Promise { } } } - -async function invokeVersionedRequestGetPublicKey( - dbCom: DbFederatedCommunity, -): Promise { - switch (dbCom.apiVersion) { - case ApiVersionType.V1_0: - return v1_0_requestGetPublicKey(dbCom) - case ApiVersionType.V1_1: - return v1_1_requestGetPublicKey(dbCom) - default: - return undefined - } -} From 2abf228ca9f943f9f3e73c748f9526ffe7d651a3 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Thu, 4 May 2023 22:53:55 +0200 Subject: [PATCH 075/153] merge --- backend/src/federation/client/Client_1_0.ts | 26 ++++++----- backend/src/federation/validateCommunities.ts | 46 ++++++------------- 2 files changed, 30 insertions(+), 42 deletions(-) diff --git a/backend/src/federation/client/Client_1_0.ts b/backend/src/federation/client/Client_1_0.ts index 456196bcb..77a72cce8 100644 --- a/backend/src/federation/client/Client_1_0.ts +++ b/backend/src/federation/client/Client_1_0.ts @@ -26,21 +26,25 @@ export class Client_1_0 { } getPublicKey = async (): Promise => { - logger.info(`requestGetPublicKey with endpoint='${this.endpoint}'...`) + logger.info('Federation: getPublicKey from endpoint', this.endpoint) try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { data, errors, headers, status } = await this.client.rawRequest(getPublicKey, {}) - logger.debug(`Response-Data:`, data, errors, headers, status) - if (data) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - logger.debug(`Response-PublicKey:`, data.getPublicKey.publicKey) - logger.info(`requestGetPublicKey processed successfully`) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access - return data.getPublicKey.publicKey + const { data } = await this.client.rawRequest(getPublicKey, {}) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (!data?.getPublicKey?.publicKey) { + logger.warn('Federation: getPublicKey without response data from endpoint', this.endpoint) + return } - logger.warn(`requestGetPublicKey processed without response data`) + logger.info( + 'Federation: getPublicKey successfull 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) { - throw new LogError(`Request-Error:`, err) + logger.warn('Federation: getPublicKey failed for endpoint', this.endpoint) } } } diff --git a/backend/src/federation/validateCommunities.ts b/backend/src/federation/validateCommunities.ts index 6879f98e7..fcce9d0d8 100644 --- a/backend/src/federation/validateCommunities.ts +++ b/backend/src/federation/validateCommunities.ts @@ -3,7 +3,6 @@ import { IsNull } from '@dbTools/typeorm' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' -import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' import { Client } from './client/Client' @@ -33,39 +32,24 @@ export async function validateCommunities(): Promise { logger.debug('Federation: dbCom', dbCom) const apiValueStrings: string[] = Object.values(ApiVersionType) logger.debug(`suppported ApiVersions=`, apiValueStrings) - if (apiValueStrings.includes(dbCom.apiVersion)) { - logger.debug( - `Federation: validate publicKey for dbCom: ${dbCom.id} with apiVersion=${dbCom.apiVersion}`, - ) - try { - const pubKey = await Client.getInstance(dbCom)?.getPublicKey() - logger.info( - 'Federation: received publicKey from endpoint', + if (!apiValueStrings.includes(dbCom.apiVersion)) { + logger.warn('Federation: dbCom with unsupported apiVersion', dbCom.endPoint, dbCom.apiVersion) + 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() }) + } else { + logger.warn( + 'Federation: received not matching publicKey:', pubKey, - `${dbCom.endPoint}/${dbCom.apiVersion}`, + dbCom.publicKey.toString(), ) - if (pubKey && pubKey === dbCom.publicKey.toString()) { - logger.info(`Federation: matching publicKey: ${pubKey}`) - await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() }) - logger.debug(`Federation: updated dbCom: ${JSON.stringify(dbCom)}`) - } else { - logger.warn( - `Federation: received not matching publicKey -> received: ${ - pubKey || 'null' - }, expected: ${dbCom.publicKey.toString()} `, - ) - // DbCommunity.delete({ id: dbCom.id }) - } - } catch (err) { - if (!(err instanceof LogError)) { - logger.error(`Error:`, err) - } } - } else { - logger.warn( - `Federation: dbCom: ${dbCom.id} with unsupported apiVersion=${dbCom.apiVersion}; supported versions`, - apiValueStrings, - ) + } catch (err) { + logger.error(`Error:`, err) } } } From cd7cef4fbf0f022db1d7e7ac11099ddc176dc6a5 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Thu, 4 May 2023 12:09:12 +0200 Subject: [PATCH 076/153] typo --- backend/src/federation/client/Client_1_0.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/federation/client/Client_1_0.ts b/backend/src/federation/client/Client_1_0.ts index 77a72cce8..ad6641f1c 100644 --- a/backend/src/federation/client/Client_1_0.ts +++ b/backend/src/federation/client/Client_1_0.ts @@ -36,7 +36,7 @@ export class Client_1_0 { return } logger.info( - 'Federation: getPublicKey successfull from endpoint', + 'Federation: getPublicKey successful from endpoint', this.endpoint, // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access data.getPublicKey.publicKey, From 6c7243758f885282be5786ea851dfbedb9d64832 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Thu, 4 May 2023 12:26:11 +0200 Subject: [PATCH 077/153] fix tests --- .../federation/validateCommunities.test.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/backend/src/federation/validateCommunities.test.ts b/backend/src/federation/validateCommunities.test.ts index ed4897e09..77d0cc2ad 100644 --- a/backend/src/federation/validateCommunities.test.ts +++ b/backend/src/federation/validateCommunities.test.ts @@ -84,7 +84,8 @@ describe('validate Communities', () => { }) it('logs requestGetPublicKey for community api 1_0 ', () => { expect(logger.info).toBeCalledWith( - `requestGetPublicKey with endpoint='http//localhost:5001/api/1_0/'...`, + 'Federation: getPublicKey from endpoint', + 'http//localhost:5001/api/1_0/', ) }) }) @@ -114,12 +115,14 @@ describe('validate Communities', () => { }) it('logs requestGetPublicKey for community api 1_0 ', () => { expect(logger.info).toBeCalledWith( - `requestGetPublicKey with endpoint='http//localhost:5001/api/1_0/'...`, + 'Federation: getPublicKey from endpoint', + 'http//localhost:5001/api/1_0/', ) }) it('logs requestGetPublicKey for community api 1_1 ', () => { expect(logger.info).toBeCalledWith( - `requestGetPublicKey with endpoint='http//localhost:5001/api/1_1/'...`, + 'Federation: getPublicKey from endpoint', + 'http//localhost:5001/api/1_1/', ) }) }) @@ -152,18 +155,21 @@ describe('validate Communities', () => { }) it('logs requestGetPublicKey for community api 1_0 ', () => { expect(logger.info).toBeCalledWith( - `requestGetPublicKey with endpoint='http//localhost:5001/api/1_0/'...`, + 'Federation: getPublicKey from endpoint', + 'http//localhost:5001/api/1_0/', ) }) it('logs requestGetPublicKey for community api 1_1 ', () => { expect(logger.info).toBeCalledWith( - `requestGetPublicKey with endpoint='http//localhost:5001/api/1_1/'...`, + 'Federation: getPublicKey from endpoint', + 'http//localhost:5001/api/1_1/', ) }) it('logs unsupported api for community with api 2_0 ', () => { expect(logger.warn).toBeCalledWith( - `Federation: dbCom: ${dbCom.id} with unsupported apiVersion=2_0; supported versions`, - ['1_0', '1_1'], + 'Federation: dbCom with unsupported apiVersion', + dbCom.endPoint, + '2_0', ) }) }) From 97b3a88065c6f7d899e745b309d991e664451eaa Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Thu, 4 May 2023 12:27:41 +0200 Subject: [PATCH 078/153] 86% line coverage --- backend/jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/jest.config.js b/backend/jest.config.js index ca12668fa..3b251916a 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -7,7 +7,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], coverageThreshold: { global: { - lines: 85, + lines: 86, }, }, setupFiles: ['/test/testSetup.ts'], From d3ba23e1e4c5da32cde11a39bea2070ffaf20f4a Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Thu, 4 May 2023 12:56:46 +0200 Subject: [PATCH 079/153] fix lint --- backend/src/federation/client/Client_1_0.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/federation/client/Client_1_0.ts b/backend/src/federation/client/Client_1_0.ts index ad6641f1c..0c0d458c8 100644 --- a/backend/src/federation/client/Client_1_0.ts +++ b/backend/src/federation/client/Client_1_0.ts @@ -2,7 +2,6 @@ import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCom import { GraphQLClient } from 'graphql-request' import { getPublicKey } from '@/federation/query/getPublicKey' -import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' // eslint-disable-next-line camelcase From 2bf839f36212216dc970f32381247c890b09e082 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Thu, 4 May 2023 13:51:08 +0200 Subject: [PATCH 080/153] lint fixes From 2cc45d23f7bac3b51d87e2203491469967ee65af Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Fri, 5 May 2023 12:44:42 +0200 Subject: [PATCH 081/153] fix between store problems --- admin/src/store/store.js | 1 + frontend/src/store/store.js | 1 + 2 files changed, 2 insertions(+) diff --git a/admin/src/store/store.js b/admin/src/store/store.js index 7d43c8ce8..0263cf33f 100644 --- a/admin/src/store/store.js +++ b/admin/src/store/store.js @@ -37,6 +37,7 @@ export const actions = { const store = new Vuex.Store({ plugins: [ createPersistedState({ + key: 'gradido-admin', storage: window.localStorage, }), ], diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js index 1cd874c06..2f35e68c5 100644 --- a/frontend/src/store/store.js +++ b/frontend/src/store/store.js @@ -91,6 +91,7 @@ try { store = new Vuex.Store({ plugins: [ createPersistedState({ + key: 'gradido-frontend', storage: window.localStorage, }), ], From 64d0200f99b810038ad8d176e320d08aba81f870 Mon Sep 17 00:00:00 2001 From: elweyn Date: Fri, 5 May 2023 13:11:36 +0200 Subject: [PATCH 082/153] Rename klicktippSignIn to subscribe. --- backend/src/apis/KlicktippController.ts | 2 +- backend/src/graphql/resolver/KlicktippResolver.ts | 4 ++-- backend/src/graphql/resolver/UserResolver.test.ts | 6 +++--- backend/src/graphql/resolver/UserResolver.ts | 6 +++--- backend/src/middleware/klicktippMiddleware.ts | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/src/apis/KlicktippController.ts b/backend/src/apis/KlicktippController.ts index a4b5b6be7..7ad1d0c2c 100644 --- a/backend/src/apis/KlicktippController.ts +++ b/backend/src/apis/KlicktippController.ts @@ -12,7 +12,7 @@ import KlicktippConnector from 'klicktipp-api' const klicktippConnector = new KlicktippConnector() -export const klicktippSignIn = async ( +export const subscribe = async ( email: string, language: string, firstName?: string, diff --git a/backend/src/graphql/resolver/KlicktippResolver.ts b/backend/src/graphql/resolver/KlicktippResolver.ts index 6875abcc5..81290bdb6 100644 --- a/backend/src/graphql/resolver/KlicktippResolver.ts +++ b/backend/src/graphql/resolver/KlicktippResolver.ts @@ -1,6 +1,6 @@ import { Resolver, Authorized, Mutation, Ctx } from 'type-graphql' -import { unsubscribe, klicktippSignIn } from '@/apis/KlicktippController' +import { unsubscribe, subscribe } from '@/apis/KlicktippController' import { RIGHTS } from '@/auth/RIGHTS' import { EVENT_NEWSLETTER_SUBSCRIBE, EVENT_NEWSLETTER_UNSUBSCRIBE } from '@/event/Events' import { Context, getUser } from '@/server/context' @@ -20,6 +20,6 @@ export class KlicktippResolver { async subscribeNewsletter(@Ctx() context: Context): Promise { const user = getUser(context) await EVENT_NEWSLETTER_SUBSCRIBE(user) - return klicktippSignIn(user.emailContact.email, user.language) + return subscribe(user.emailContact.email, user.language) } } diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index d60ba7771..6051d83b7 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -61,7 +61,7 @@ import { stephenHawking } from '@/seeds/users/stephen-hawking' import { printTimeDuration } from '@/util/time' import { objectValuesToArray } from '@/util/utilities' -// import { klicktippSignIn } from '@/apis/KlicktippController' +// import { subscribe } from '@/apis/KlicktippController' jest.mock('@/emails/sendEmailVariants', () => { const originalModule = jest.requireActual('@/emails/sendEmailVariants') @@ -81,7 +81,7 @@ jest.mock('@/emails/sendEmailVariants', () => { jest.mock('@/apis/KlicktippController', () => { return { __esModule: true, - klicktippSignIn: jest.fn(), + subscribe: jest.fn(), } }) */ @@ -558,7 +558,7 @@ describe('UserResolver', () => { /* it('calls the klicktipp API', () => { - expect(klicktippSignIn).toBeCalledWith( + expect(subscribe).toBeCalledWith( user[0].email, user[0].language, user[0].firstName, diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 60b4403af..ced4f8b3c 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -35,7 +35,7 @@ import { User } from '@model/User' import { UserAdmin, SearchUsersResult } from '@model/UserAdmin' import { UserRepository } from '@repository/User' -import { klicktippSignIn } from '@/apis/KlicktippController' +import { subscribe } from '@/apis/KlicktippController' import { encode } from '@/auth/JWT' import { RIGHTS } from '@/auth/RIGHTS' import { CONFIG } from '@/config' @@ -469,9 +469,9 @@ export class UserResolver { // TODO do we always signUp the user? How to handle things with old users? if (userContact.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) { try { - await klicktippSignIn(userContact.email, user.language, user.firstName, user.lastName) + await subscribe(userContact.email, user.language, user.firstName, user.lastName) logger.debug( - `klicktippSignIn(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`, + `subscribe(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`, ) } catch (e) { logger.error('Error subscribing to klicktipp', e) diff --git a/backend/src/middleware/klicktippMiddleware.ts b/backend/src/middleware/klicktippMiddleware.ts index 4c5f8db4f..c988c6d7a 100644 --- a/backend/src/middleware/klicktippMiddleware.ts +++ b/backend/src/middleware/klicktippMiddleware.ts @@ -19,7 +19,7 @@ import { klickTippLogger as logger } from '@/server/logger' // // Do Something here before resolver is called // const result = await next() // // Do Something here after resolver is completed -// await klicktippSignIn(result.email, result.language, result.firstName, result.lastName) +// await subscribe(result.email, result.language, result.firstName, result.lastName) // return result // } From 67d1ae7d1b2df4f30bac71d68db7840b77a3e4ce Mon Sep 17 00:00:00 2001 From: mahula Date: Fri, 5 May 2023 13:26:16 +0200 Subject: [PATCH 083/153] update jest-canvas-mock version to resolve window mock problem in tests --- frontend/package.json | 2 +- frontend/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 8515ca209..f07284da6 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -44,7 +44,7 @@ "graphql": "^15.5.1", "identity-obj-proxy": "^3.0.0", "jest": "^26.6.3", - "jest-canvas-mock": "^2.3.1", + "jest-canvas-mock": "^2.5.0", "jwt-decode": "^3.1.2", "portal-vue": "^2.1.7", "prettier": "^2.2.1", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 9e8fdd81d..7cc8e5fe5 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -8657,10 +8657,10 @@ javascript-stringify@^1.6.0: resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-1.6.0.tgz#142d111f3a6e3dae8f4a9afd77d45855b5a9cce3" integrity sha1-FC0RHzpuPa6PSpr9d9RYVbWpzOM= -jest-canvas-mock@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.3.1.tgz#9535d14bc18ccf1493be36ac37dd349928387826" - integrity sha512-5FnSZPrX3Q2ZfsbYNE3wqKR3+XorN8qFzDzB5o0golWgt6EOX1+emBnpOc9IAQ+NXFj8Nzm3h7ZdE/9H0ylBcg== +jest-canvas-mock@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.5.0.tgz#3e60f87f77ddfa273cf8e7e4ea5f86fa827c7117" + integrity sha512-s2bmY2f22WPMzhB2YA93kiyf7CAfWAnV/sFfY9s48IVOrGmwui1eSFluDPesq1M+7tSC1hJAit6mzO0ZNXvVBA== dependencies: cssfontparser "^1.2.1" moo-color "^1.0.2" From 4de3daf12d5befae013ef2acada7fd3cba8c5cbb Mon Sep 17 00:00:00 2001 From: elweyn Date: Fri, 5 May 2023 13:35:48 +0200 Subject: [PATCH 084/153] Remove unused method. --- backend/src/apis/KlicktippController.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/backend/src/apis/KlicktippController.ts b/backend/src/apis/KlicktippController.ts index 7ad1d0c2c..2e71f2cb6 100644 --- a/backend/src/apis/KlicktippController.ts +++ b/backend/src/apis/KlicktippController.ts @@ -28,13 +28,6 @@ export const subscribe = async ( return result } -export const signout = async (email: string, language: string): Promise => { - if (!CONFIG.KLICKTIPP) return true - const apiKey = language === 'de' ? CONFIG.KLICKTIPP_APIKEY_DE : CONFIG.KLICKTIPP_APIKEY_EN - const result = await klicktippConnector.signoff(apiKey, email) - return result -} - export const unsubscribe = async (email: string): Promise => { if (!CONFIG.KLICKTIPP) return true const isLogin = await loginKlicktippUser() From 2286700b833013968667abe9ed079e37f828f06c Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Fri, 5 May 2023 14:18:30 +0200 Subject: [PATCH 085/153] fix e2e test --- e2e-tests/cypress/support/e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e-tests/cypress/support/e2e.ts b/e2e-tests/cypress/support/e2e.ts index 2f3557566..6866cae3e 100644 --- a/e2e-tests/cypress/support/e2e.ts +++ b/e2e-tests/cypress/support/e2e.ts @@ -35,6 +35,6 @@ Cypress.Commands.add('login', (email, password) => { } cy.visit('/') - window.localStorage.setItem('vuex', JSON.stringify(vuexToken)) + window.localStorage.setItem('gradido-frontend', JSON.stringify(vuexToken)) }) }) From b8e015db222c9a4b8ce6e076025c309e9b0a5e45 Mon Sep 17 00:00:00 2001 From: elweyn Date: Fri, 5 May 2023 16:17:47 +0200 Subject: [PATCH 086/153] Remove unused method. --- backend/src/apis/KlicktippController.ts | 32 ------------------------- 1 file changed, 32 deletions(-) diff --git a/backend/src/apis/KlicktippController.ts b/backend/src/apis/KlicktippController.ts index 2e71f2cb6..a2a8f86cb 100644 --- a/backend/src/apis/KlicktippController.ts +++ b/backend/src/apis/KlicktippController.ts @@ -53,38 +53,6 @@ export const loginKlicktippUser = async (): Promise => { return await klicktippConnector.login(CONFIG.KLICKTIPP_USER, CONFIG.KLICKTIPP_PASSWORD) } -export const logoutKlicktippUser = async (): Promise => { - if (!CONFIG.KLICKTIPP) return true - return await klicktippConnector.logout() -} - -export const untagUser = async (email: string, tagId: string): Promise => { - if (!CONFIG.KLICKTIPP) return true - const isLogin = await loginKlicktippUser() - if (isLogin) { - return await klicktippConnector.untag(email, tagId) - } - return false -} - -export const tagUser = async (email: string, tagIds: string): Promise => { - if (!CONFIG.KLICKTIPP) return true - const isLogin = await loginKlicktippUser() - if (isLogin) { - return await klicktippConnector.tag(email, tagIds) - } - return false -} - -export const getKlicktippTagMap = async () => { - if (!CONFIG.KLICKTIPP) return true - const isLogin = await loginKlicktippUser() - if (isLogin) { - return await klicktippConnector.tagIndex() - } - return '' -} - export const addFieldsToSubscriber = async ( email: string, fields: any = {}, From 4e78dcddc191c503a0701760183d1ad7faa46746 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Fri, 5 May 2023 17:18:53 +0200 Subject: [PATCH 087/153] install jose, remove jsonwebtoken --- backend/package.json | 3 +- backend/yarn.lock | 96 ++++---------------------------------------- 2 files changed, 8 insertions(+), 91 deletions(-) diff --git a/backend/package.json b/backend/package.json index c5e0df3c5..02aed0161 100644 --- a/backend/package.json +++ b/backend/package.json @@ -33,7 +33,7 @@ "graphql": "^15.5.1", "graphql-request": "5.0.0", "i18n": "^0.15.1", - "jsonwebtoken": "^8.5.1", + "jose": "^4.14.4", "lodash.clonedeep": "^4.5.0", "log4js": "^6.4.6", "mysql2": "^2.3.0", @@ -51,7 +51,6 @@ "@types/faker": "^5.5.9", "@types/i18n": "^0.13.4", "@types/jest": "^27.0.2", - "@types/jsonwebtoken": "^8.5.2", "@types/lodash.clonedeep": "^4.5.6", "@types/node": "^16.10.3", "@types/nodemailer": "^6.4.4", diff --git a/backend/yarn.lock b/backend/yarn.lock index 1bc8c64fd..d33eb0469 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1051,13 +1051,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@types/jsonwebtoken@^8.5.2": - version "8.5.5" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.5.tgz#da5f2f4baee88f052ef3e4db4c1a0afb46cff22c" - integrity sha512-OGqtHQ7N5/Ap/TUwO6IgHDuLiAoTmHhGpNvgkCm/F4N6pKzx/RBSfr2OXZSwC6vkfnsEdb6+7DNZVtiXiwdwFw== - dependencies: - "@types/node" "*" - "@types/keygrip@*": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" @@ -1994,11 +1987,6 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= - buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -2691,13 +2679,6 @@ duplexer3@^0.1.4: resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= -ecdsa-sig-formatter@1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -3649,7 +3630,7 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0: integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== "gradido-database@file:../database": - version "1.19.1" + version "1.20.0" dependencies: "@types/uuid" "^8.3.4" cross-env "^7.0.3" @@ -4790,6 +4771,11 @@ jest@^27.2.4: import-local "^3.0.2" jest-cli "^27.2.5" +jose@^4.14.4: + version "4.14.4" + resolved "https://registry.yarnpkg.com/jose/-/jose-4.14.4.tgz#59e09204e2670c3164ee24cbfe7115c6f8bff9ca" + integrity sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g== + js-sdsl@^4.1.4: version "4.3.0" resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711" @@ -4903,22 +4889,6 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -jsonwebtoken@^8.5.1: - version "8.5.1" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" - integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== - dependencies: - jws "^3.2.2" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" - ms "^2.1.1" - semver "^5.6.0" - jstransformer@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3" @@ -4938,23 +4908,6 @@ juice@^8.0.0: slick "^1.12.2" web-resource-inliner "^6.0.1" -jwa@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" - integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" - integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== - dependencies: - jwa "^1.4.1" - safe-buffer "^5.0.1" - keyv@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" @@ -5058,46 +5011,11 @@ lodash.get@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" - integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= - -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" - integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= - -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" - integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= - -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" - integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= - lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.once@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" - integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= - lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -6317,7 +6235,7 @@ semver@7.x, semver@^7.3.2, semver@^7.3.4: dependencies: lru-cache "^6.0.0" -semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: +semver@^5.5.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== From 36d40cbe4086783ed96228f93d06adc4f798866c Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Fri, 5 May 2023 17:19:15 +0200 Subject: [PATCH 088/153] new jwt functions --- backend/src/auth/CustomJwtPayload.ts | 4 ++-- backend/src/auth/JWT.ts | 25 ++++++++++++++++++------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/backend/src/auth/CustomJwtPayload.ts b/backend/src/auth/CustomJwtPayload.ts index 7966b413e..e20e5b272 100644 --- a/backend/src/auth/CustomJwtPayload.ts +++ b/backend/src/auth/CustomJwtPayload.ts @@ -1,5 +1,5 @@ -import { JwtPayload } from 'jsonwebtoken' +import { JWTPayload } from 'jose' -export interface CustomJwtPayload extends JwtPayload { +export interface CustomJwtPayload extends JWTPayload { gradidoID: string } diff --git a/backend/src/auth/JWT.ts b/backend/src/auth/JWT.ts index 75a69cd0c..4f5d645c2 100644 --- a/backend/src/auth/JWT.ts +++ b/backend/src/auth/JWT.ts @@ -1,22 +1,33 @@ -import { verify, sign } from 'jsonwebtoken' +import { SignJWT, jwtVerify } from 'jose' import { CONFIG } from '@/config/' import { LogError } from '@/server/LogError' import { CustomJwtPayload } from './CustomJwtPayload' -export const decode = (token: string): CustomJwtPayload | null => { +export const decode = async (token: string): Promise => { if (!token) throw new LogError('401 Unauthorized') + try { - return verify(token, CONFIG.JWT_SECRET) + const secret = new TextEncoder().encode(CONFIG.JWT_SECRET) + const { payload } = await jwtVerify(token, secret, { + issuer: 'urn:example:issuer', // TODO urn + audience: 'urn:example:audience', // TODO urn + }) + return payload as CustomJwtPayload } catch (err) { return null } } -export const encode = (gradidoID: string): string => { - const token = sign({ gradidoID }, CONFIG.JWT_SECRET, { - expiresIn: CONFIG.JWT_EXPIRES_IN, - }) +export const encode = async (gradidoID: string): Promise => { + const secret = new TextEncoder().encode(CONFIG.JWT_SECRET) + const token = await new SignJWT({ gradidoID, 'urn:example:claim': true }) // TODO urn + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setIssuer('urn:example:issuer') // TODO urn + .setAudience('urn:example:audience') // TODO urn + .setExpirationTime(CONFIG.JWT_EXPIRES_IN) + .sign(secret) return token } From 4f0910baf144442c03afe119c8968dafe84d87c0 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Fri, 5 May 2023 17:19:28 +0200 Subject: [PATCH 089/153] jwt functions are async now, await them --- backend/src/graphql/directive/isAuthorized.ts | 4 ++-- backend/src/graphql/resolver/UserResolver.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/graphql/directive/isAuthorized.ts b/backend/src/graphql/directive/isAuthorized.ts index b8595a2bd..ddd78f1fa 100644 --- a/backend/src/graphql/directive/isAuthorized.ts +++ b/backend/src/graphql/directive/isAuthorized.ts @@ -21,7 +21,7 @@ export const isAuthorized: AuthChecker = async ({ context }, rights) => } // Decode the token - const decoded = decode(context.token) + const decoded = await decode(context.token) if (!decoded) { throw new LogError('403.13 - Client certificate revoked') } @@ -49,6 +49,6 @@ export const isAuthorized: AuthChecker = async ({ context }, rights) => } // set new header token - context.setHeaders.push({ key: 'token', value: encode(decoded.gradidoID) }) + context.setHeaders.push({ key: 'token', value: await encode(decoded.gradidoID) }) return true } diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 60b4403af..918b65885 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -185,7 +185,7 @@ export class UserResolver { context.setHeaders.push({ key: 'token', - value: encode(dbUser.gradidoID), + value: await encode(dbUser.gradidoID), }) await EVENT_USER_LOGIN(dbUser) From ad4418d29784c0d3c75777bdfcfd24e95cad4e96 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 8 May 2023 09:35:14 +0200 Subject: [PATCH 090/153] test transaction list query to raise coverage --- .../resolver/TransactionResolver.test.ts | 43 +++++++++++++++++++ backend/src/seeds/graphql/queries.ts | 26 +++++------ 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index 1a2f04838..24fa4e48c 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -20,12 +20,15 @@ import { login, sendCoins, } from '@/seeds/graphql/mutations' +import { transactionsQuery } from '@/seeds/graphql/queries' import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { garrickOllivander } from '@/seeds/users/garrick-ollivander' import { peterLustig } from '@/seeds/users/peter-lustig' import { stephenHawking } from '@/seeds/users/stephen-hawking' let mutate: ApolloServerTestClient['mutate'], con: Connection +let query: ApolloServerTestClient['query'] + let testEnv: { mutate: ApolloServerTestClient['mutate'] query: ApolloServerTestClient['query'] @@ -35,6 +38,7 @@ let testEnv: { beforeAll(async () => { testEnv = await testEnvironment(logger) mutate = testEnv.mutate + query = testEnv.query con = testEnv.con await cleanDB() }) @@ -442,3 +446,42 @@ describe('send coins', () => { }) }) }) + +describe('transactionList', () => { + describe('unauthenticated', () => { + it('throws an error', async () => { + await expect(query({ query: transactionsQuery })).resolves.toMatchObject({ + errors: [new GraphQLError('401 Unauthorized')], + }) + }) + }) + + describe('authenticated', () => { + describe('no transactions', () => { + beforeAll(async () => { + await userFactory(testEnv, bobBaumeister) + await mutate({ + mutation: login, + variables: { + email: 'bob@baumeister.de', + password: 'Aa12345_', + }, + }) + }) + + it('has no transactions and balance 0', async () => { + await expect(query({ query: transactionsQuery })).resolves.toMatchObject({ + data: { + transactionList: { + balance: expect.objectContaining({ + balance: expect.decimalEqual(0), + }), + transactions: [], + }, + }, + errors: undefined, + }) + }) + }) + }) +}) diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index cc1edbc9d..bc8fa95e8 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -23,31 +23,26 @@ export const queryOptIn = gql` ` export const transactionsQuery = gql` - query ( - $currentPage: Int = 1 - $pageSize: Int = 25 - $order: Order = DESC - $onlyCreations: Boolean = false - ) { - transactionList( - currentPage: $currentPage - pageSize: $pageSize - order: $order - onlyCreations: $onlyCreations - ) { - balanceGDT - count - balance + query ($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) { + transactionList(currentPage: $currentPage, pageSize: $pageSize, order: $order) { + balance { + balance + balanceGDT + count + linkCount + } transactions { id typeId amount balance + previousBalance balanceDate memo linkedUser { firstName lastName + gradidoID } decay { decay @@ -55,6 +50,7 @@ export const transactionsQuery = gql` end duration } + linkId } } } From 0cd2b7646d1190825612f5637db1abe78ea5e354 Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Mon, 8 May 2023 11:03:01 +0200 Subject: [PATCH 091/153] rework PR-comments --- backend/src/util/utilities.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/src/util/utilities.ts b/backend/src/util/utilities.ts index 4dc71ff7f..30c1effdb 100644 --- a/backend/src/util/utilities.ts +++ b/backend/src/util/utilities.ts @@ -19,14 +19,14 @@ export const fullName = (firstName: string, lastName: string): string => { return [firstName, lastName].filter(Boolean).join(' ') } -export const userName = (f?: string, l?: string): string | null => { +export const userName = (firstName?: string, lastName?: string): string | null => { let name: string | null - if (f && l) { - name = f + ' ' + l - } else if (f && !l) { - name = f - } else if (!f && l) { - name = l + if (firstName && lastName) { + name = firstName + ' ' + lastName + } else if (firstName && !lastName) { + name = firstName + } else if (!firstName && lastName) { + name = lastName } else { name = null } From 740b5cc83fda0b3f14a46bbc99209c0f0d5c9989 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Mon, 8 May 2023 11:45:14 +0200 Subject: [PATCH 092/153] admin: rename dist folder to build --- admin/.eslintignore | 2 +- admin/.gitignore | 2 +- admin/Dockerfile | 2 +- admin/package.json | 2 +- admin/run/server.js | 4 ++-- admin/vue.config.js | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/admin/.eslintignore b/admin/.eslintignore index 94934f9e7..e19e2338d 100644 --- a/admin/.eslintignore +++ b/admin/.eslintignore @@ -1,3 +1,3 @@ node_modules/ -dist/ +build/ coverage/ \ No newline at end of file diff --git a/admin/.gitignore b/admin/.gitignore index a67d270bc..3bc7bb4b2 100644 --- a/admin/.gitignore +++ b/admin/.gitignore @@ -1,5 +1,5 @@ node_modules/ -dist/ +build/ .cache/ /.env diff --git a/admin/Dockerfile b/admin/Dockerfile index ed0623a63..029339f10 100644 --- a/admin/Dockerfile +++ b/admin/Dockerfile @@ -84,7 +84,7 @@ CMD /bin/sh -c "yarn run dev" FROM base as production # Copy "binary"-files from build image -COPY --from=build ${DOCKER_WORKDIR}/dist ./dist +COPY --from=build ${DOCKER_WORKDIR}/build ./build # We also copy the node_modules express and serve-static for the run script COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules # Copy static files diff --git a/admin/package.json b/admin/package.json index 04c9a60e8..7a3b36a7a 100644 --- a/admin/package.json +++ b/admin/package.json @@ -11,7 +11,7 @@ "serve": "vue-cli-service serve --open", "build": "vue-cli-service build", "dev": "yarn run serve", - "analyse-bundle": "yarn build && webpack-bundle-analyzer dist/webpack.stats.json", + "analyse-bundle": "yarn build && webpack-bundle-analyzer build/webpack.stats.json", "lint": "eslint --max-warnings=0 --ext .js,.vue,.json .", "stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'", "test": "cross-env TZ=UTC jest", diff --git a/admin/run/server.js b/admin/run/server.js index bccefc65c..b5078a0cf 100644 --- a/admin/run/server.js +++ b/admin/run/server.js @@ -9,10 +9,10 @@ const port = process.env.PORT || 8080 // Express Server const app = express() // Serve files -app.use(express.static(path.join(__dirname, '../dist'))) +app.use(express.static(path.join(__dirname, '../build'))) // Default to index.html app.get('*', (req, res) => { - res.sendFile(path.join(__dirname, '../dist/index.html')) + res.sendFile(path.join(__dirname, '../build/index.html')) }) app.listen(port, hostname, () => { diff --git a/admin/vue.config.js b/admin/vue.config.js index 8cc1e4b89..a92b26419 100644 --- a/admin/vue.config.js +++ b/admin/vue.config.js @@ -49,5 +49,5 @@ module.exports = { // Enable CSS source maps. sourceMap: CONFIG.NODE_ENV !== 'production', }, - outputDir: path.resolve(__dirname, './dist'), + outputDir: path.resolve(__dirname, './build'), } From b9cded91624b8db1bd324886677213bf5fdabe4b Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Mon, 8 May 2023 11:45:59 +0200 Subject: [PATCH 093/153] frontend: rename dist fodler to build --- frontend/.eslintignore | 2 +- frontend/.gitignore | 2 +- frontend/Dockerfile | 2 +- frontend/package.json | 2 +- frontend/run/server.js | 4 ++-- frontend/vue.config.js | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/.eslintignore b/frontend/.eslintignore index 94934f9e7..e19e2338d 100644 --- a/frontend/.eslintignore +++ b/frontend/.eslintignore @@ -1,3 +1,3 @@ node_modules/ -dist/ +build/ coverage/ \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore index 0a541ba06..843f840a1 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,6 +1,6 @@ .DS_Store node_modules/ -dist/ +build/ .cache/ npm-debug.log* yarn-debug.log* diff --git a/frontend/Dockerfile b/frontend/Dockerfile index a93199fad..1a4d8ca5c 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -84,7 +84,7 @@ CMD /bin/sh -c "yarn run dev" FROM base as production # Copy "binary"-files from build image -COPY --from=build ${DOCKER_WORKDIR}/dist ./dist +COPY --from=build ${DOCKER_WORKDIR}/build ./build # We also copy the node_modules express and serve-static for the run script COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules # Copy static files diff --git a/frontend/package.json b/frontend/package.json index f07284da6..7d89fba7c 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,7 @@ "serve": "vue-cli-service serve --open", "build": "vue-cli-service build", "dev": "yarn run serve", - "analyse-bundle": "yarn build && webpack-bundle-analyzer dist/webpack.stats.json", + "analyse-bundle": "yarn build && webpack-bundle-analyzer build/webpack.stats.json", "lint": "eslint --max-warnings=0 --ext .js,.vue,.json .", "stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'", "test": "cross-env TZ=UTC jest", diff --git a/frontend/run/server.js b/frontend/run/server.js index 7d75acba8..4a49dcd75 100644 --- a/frontend/run/server.js +++ b/frontend/run/server.js @@ -9,10 +9,10 @@ const port = process.env.PORT || 3000 // Express Server const app = express() // Serve files -app.use(express.static(path.join(__dirname, '../dist'))) +app.use(express.static(path.join(__dirname, '../build'))) // Default to index.html app.get('*', (req, res) => { - res.sendFile(path.join(__dirname, '../dist/index.html')) + res.sendFile(path.join(__dirname, '../build/index.html')) }) app.listen(port, hostname, () => { diff --git a/frontend/vue.config.js b/frontend/vue.config.js index 573f2a70b..7fb5d5fc2 100644 --- a/frontend/vue.config.js +++ b/frontend/vue.config.js @@ -64,5 +64,5 @@ module.exports = { // Enable CSS source maps. sourceMap: CONFIG.NODE_ENV !== 'production', }, - outputDir: path.resolve(__dirname, './dist'), + outputDir: path.resolve(__dirname, './build'), } From 60a86da5d2a691dc6143a238bc6df6ad3bd9a995 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Mon, 8 May 2023 11:46:37 +0200 Subject: [PATCH 094/153] corrected paths in scripts --- .../bare_metal/nginx/sites-available/gradido.conf.ssl.template | 2 +- .../bare_metal/nginx/sites-available/gradido.conf.template | 2 +- deployment/bare_metal/old/build_frontend.sh | 2 +- nginx/gradido.conf | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template b/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template index ddb0724b0..a99327745 100644 --- a/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template +++ b/deployment/bare_metal/nginx/sites-available/gradido.conf.ssl.template @@ -117,7 +117,7 @@ server { # TODO this could be a performance optimization #location /vue { - # alias /var/www/html/gradido/frontend/dist; + # alias /var/www/html/gradido/frontend/build; # index index.html; # # location ~* \.(png)$ { diff --git a/deployment/bare_metal/nginx/sites-available/gradido.conf.template b/deployment/bare_metal/nginx/sites-available/gradido.conf.template index 42a5a1851..f6149a818 100644 --- a/deployment/bare_metal/nginx/sites-available/gradido.conf.template +++ b/deployment/bare_metal/nginx/sites-available/gradido.conf.template @@ -103,7 +103,7 @@ server { # TODO this could be a performance optimization #location /vue { - # alias /var/www/html/gradido/frontend/dist; + # alias /var/www/html/gradido/frontend/build; # index index.html; # # location ~* \.(png)$ { diff --git a/deployment/bare_metal/old/build_frontend.sh b/deployment/bare_metal/old/build_frontend.sh index 4b3a8a444..a9103c19f 100755 --- a/deployment/bare_metal/old/build_frontend.sh +++ b/deployment/bare_metal/old/build_frontend.sh @@ -15,6 +15,6 @@ export NVM_DIR="/root/.nvm" $NPM_BIN install $NPM_BIN run build # prezip for faster deliver throw nginx -cd dist +cd build find . -type f -name "*.css" -exec gzip -9 -k {} \; find . -type f -name "*.js" -exec gzip -9 -k {} \; diff --git a/nginx/gradido.conf b/nginx/gradido.conf index bfb079d32..403a2766b 100644 --- a/nginx/gradido.conf +++ b/nginx/gradido.conf @@ -71,7 +71,7 @@ server { # TODO this could be a performance optimization #location /vue { - # alias /var/www/html/gradido/frontend/dist; + # alias /var/www/html/gradido/frontend/build; # index index.html; # # location ~* \.(png)$ { From b71f8c10c75cc1975bd69099f579876c7aff24f3 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Mon, 8 May 2023 11:47:31 +0200 Subject: [PATCH 095/153] remove all build folders in start script --- deployment/bare_metal/start.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/deployment/bare_metal/start.sh b/deployment/bare_metal/start.sh index b30d90f19..5d5744bd6 100755 --- a/deployment/bare_metal/start.sh +++ b/deployment/bare_metal/start.sh @@ -130,6 +130,15 @@ rm -Rf $PROJECT_ROOT/admin/node_modules rm -Rf $PROJECT_ROOT/dht-node/node_modules rm -Rf $PROJECT_ROOT/federation/node_modules +# Remove build folders +# we had problems with corrupted incremtal builds +rm -Rf $PROJECT_ROOT/database/build +rm -Rf $PROJECT_ROOT/backend/build +rm -Rf $PROJECT_ROOT/frontend/build +rm -Rf $PROJECT_ROOT/admin/build +rm -Rf $PROJECT_ROOT/dht-node/build +rm -Rf $PROJECT_ROOT/federation/build + # Regenerate .env files cp -f $PROJECT_ROOT/database/.env $PROJECT_ROOT/database/.env.bak cp -f $PROJECT_ROOT/backend/.env $PROJECT_ROOT/backend/.env.bak From 09150082e83a23dbca15aeeead59fb952a49c6fb Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Mon, 8 May 2023 12:08:01 +0200 Subject: [PATCH 096/153] use urn:gradido --- backend/src/auth/JWT.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/src/auth/JWT.ts b/backend/src/auth/JWT.ts index 4f5d645c2..d8c6c81cf 100644 --- a/backend/src/auth/JWT.ts +++ b/backend/src/auth/JWT.ts @@ -11,8 +11,8 @@ export const decode = async (token: string): Promise => try { const secret = new TextEncoder().encode(CONFIG.JWT_SECRET) const { payload } = await jwtVerify(token, secret, { - issuer: 'urn:example:issuer', // TODO urn - audience: 'urn:example:audience', // TODO urn + issuer: 'urn:gradido:issuer', + audience: 'urn:gradido:audience', }) return payload as CustomJwtPayload } catch (err) { @@ -22,11 +22,11 @@ export const decode = async (token: string): Promise => export const encode = async (gradidoID: string): Promise => { const secret = new TextEncoder().encode(CONFIG.JWT_SECRET) - const token = await new SignJWT({ gradidoID, 'urn:example:claim': true }) // TODO urn + const token = await new SignJWT({ gradidoID, 'urn:gradido:claim': true }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() - .setIssuer('urn:example:issuer') // TODO urn - .setAudience('urn:example:audience') // TODO urn + .setIssuer('urn:gradido:issuer') + .setAudience('urn:gradido:audience') .setExpirationTime(CONFIG.JWT_EXPIRES_IN) .sign(secret) return token From 9ad3b704df9dc7b51f4231401caff3f7d71e4433 Mon Sep 17 00:00:00 2001 From: mahula Date: Mon, 8 May 2023 12:10:43 +0200 Subject: [PATCH 097/153] adapt cypress env loginQuery to changes in backend --- e2e-tests/cypress.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e-tests/cypress.config.ts b/e2e-tests/cypress.config.ts index 7205ef9cb..e26259626 100644 --- a/e2e-tests/cypress.config.ts +++ b/e2e-tests/cypress.config.ts @@ -58,7 +58,7 @@ export default defineConfig({ mailserverURL: 'http://localhost:1080', loginQuery: `mutation ($email: String!, $password: String!, $publisherId: Int) { login(email: $email, password: $password, publisherId: $publisherId) { - email + id firstName lastName language From 03246bb54a4f7169fd86a162b8572202e14390a7 Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Mon, 8 May 2023 17:03:37 +0200 Subject: [PATCH 098/153] Update backend/src/util/utilities.ts Co-authored-by: Hannes Heine --- backend/src/util/utilities.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/backend/src/util/utilities.ts b/backend/src/util/utilities.ts index 30c1effdb..df2166cfd 100644 --- a/backend/src/util/utilities.ts +++ b/backend/src/util/utilities.ts @@ -19,17 +19,3 @@ export const fullName = (firstName: string, lastName: string): string => { return [firstName, lastName].filter(Boolean).join(' ') } -export const userName = (firstName?: string, lastName?: string): string | null => { - let name: string | null - if (firstName && lastName) { - name = firstName + ' ' + lastName - } else if (firstName && !lastName) { - name = firstName - } else if (!firstName && lastName) { - name = lastName - } else { - name = null - } - - return name -} From 20daf80c44d5694d5d7c71fd1f508911114385d6 Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Mon, 8 May 2023 17:14:55 +0200 Subject: [PATCH 099/153] Update database/migrations/0066-x-community-sendcoins-transactions_table.ts Co-authored-by: Hannes Heine --- ...-community-sendcoins-transactions_table.ts | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/database/migrations/0066-x-community-sendcoins-transactions_table.ts b/database/migrations/0066-x-community-sendcoins-transactions_table.ts index 9f80460ed..a56c6df00 100644 --- a/database/migrations/0066-x-community-sendcoins-transactions_table.ts +++ b/database/migrations/0066-x-community-sendcoins-transactions_table.ts @@ -77,34 +77,6 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis `UPDATE transactions t, users u SET t.linked_user_gradido_id = u.gradido_id, t.linked_user_name = concat(u.first_name, ' ', u.last_name) WHERE t.linked_user_id = u.id and t.linked_user_id is null and t.linked_user_gradido_id is null;`, ) - await queryFn(` - CREATE TABLE IF NOT EXISTS \`pending_transactions\` ( - \`id\` int(10) unsigned NOT NULL AUTO_INCREMENT, - \`previous\` int(10) unsigned DEFAULT NULL NULL, - \`type_id\` int(10) DEFAULT NULL NULL, - \`transaction_link_id\` int(10) unsigned DEFAULT NULL NULL, - \`amount\` decimal(40,20) DEFAULT NULL NULL, - \`balance\` decimal(40,20) DEFAULT NULL NULL, - \`balance_date\` datetime(3) DEFAULT current_timestamp(3) NOT NULL, - \`decay\` decimal(40,20) DEFAULT NULL NULL, - \`decay_start\` datetime(3) DEFAULT NULL NULL, - \`memo\` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - \`creation_date\` datetime(3) DEFAULT NULL NULL, - \`user_id\` int(10) unsigned NOT NULL, - \`user_gradido_id\` char(36) NOT NULL, - \`user_community_uuid\` char(36) NOT NULL, - \`user_name\` varchar(512) COLLATE utf8mb4_unicode_ci NOT NULL, - \`linked_user_id\` int(10) unsigned DEFAULT NULL NULL, - \`linked_user_gradido_id\` char(36) NOT NULL, - \`linked_user_community_uuid\` char(36) NOT NULL, - \`linked_user_name\` varchar(512) NULL, - \`linked_transaction_id\` int(10) DEFAULT NULL NULL, - \`x_transaction_state\` varchar(100) NOT NULL COMMENT 'States to handle 2-Phase-Commit handshake', - \`created_at\` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - \`updated_at\` datetime(3) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(3), - \`deleted_at\` datetime(3) NULL DEFAULT NULL, - PRIMARY KEY (\`id\`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`) } /* eslint-disable @typescript-eslint/no-empty-function */ From 920b0f6d027a5fb362765a3044327741d1b80ed3 Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Mon, 8 May 2023 17:15:06 +0200 Subject: [PATCH 100/153] Update database/migrations/0066-x-community-sendcoins-transactions_table.ts Co-authored-by: Hannes Heine --- .../migrations/0066-x-community-sendcoins-transactions_table.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/database/migrations/0066-x-community-sendcoins-transactions_table.ts b/database/migrations/0066-x-community-sendcoins-transactions_table.ts index a56c6df00..10693bdba 100644 --- a/database/migrations/0066-x-community-sendcoins-transactions_table.ts +++ b/database/migrations/0066-x-community-sendcoins-transactions_table.ts @@ -88,5 +88,4 @@ export async function downgrade(queryFn: (query: string, values?: any[]) => Prom await queryFn('ALTER TABLE `transactions` DROP COLUMN `linked_user_gradido_id`;') await queryFn('ALTER TABLE `transactions` DROP COLUMN `linked_user_community_uuid`;') await queryFn('ALTER TABLE `transactions` DROP COLUMN `linked_user_name`;') - await queryFn(`DROP TABLE IF EXISTS pending_transactions;`) } From b87373947adbd7b047afbc9a80fc8a12e5770457 Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Tue, 9 May 2023 01:27:13 +0200 Subject: [PATCH 101/153] linting --- backend/src/util/utilities.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/util/utilities.ts b/backend/src/util/utilities.ts index df2166cfd..6bd70b941 100644 --- a/backend/src/util/utilities.ts +++ b/backend/src/util/utilities.ts @@ -18,4 +18,3 @@ export const decimalSeparatorByLanguage = (a: Decimal, language: string): string export const fullName = (firstName: string, lastName: string): string => { return [firstName, lastName].filter(Boolean).join(' ') } - From 6519b4b75e9c9f37eab79e26b44655e54c8a6c1f Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Tue, 9 May 2023 10:18:01 +0200 Subject: [PATCH 102/153] Update backend/src/federation/client/Client_1_0.ts Co-authored-by: Hannes Heine --- backend/src/federation/client/Client_1_0.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/federation/client/Client_1_0.ts b/backend/src/federation/client/Client_1_0.ts index 456196bcb..19d58de89 100644 --- a/backend/src/federation/client/Client_1_0.ts +++ b/backend/src/federation/client/Client_1_0.ts @@ -30,7 +30,7 @@ export class Client_1_0 { try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { data, errors, headers, status } = await this.client.rawRequest(getPublicKey, {}) - logger.debug(`Response-Data:`, data, errors, headers, status) + logger.debug('Response-Data:', data, errors, headers, status) if (data) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access logger.debug(`Response-PublicKey:`, data.getPublicKey.publicKey) From f8a63f9b71c2830d7d09905d634adb7c6aa898bf Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Tue, 9 May 2023 10:18:43 +0200 Subject: [PATCH 103/153] Update backend/src/federation/client/Client_1_0.ts Co-authored-by: Hannes Heine --- backend/src/federation/client/Client_1_0.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/federation/client/Client_1_0.ts b/backend/src/federation/client/Client_1_0.ts index 19d58de89..fd2f941a3 100644 --- a/backend/src/federation/client/Client_1_0.ts +++ b/backend/src/federation/client/Client_1_0.ts @@ -33,7 +33,7 @@ export class Client_1_0 { logger.debug('Response-Data:', data, errors, headers, status) if (data) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - logger.debug(`Response-PublicKey:`, data.getPublicKey.publicKey) + logger.debug('Response-PublicKey:', data.getPublicKey.publicKey) logger.info(`requestGetPublicKey processed successfully`) // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access return data.getPublicKey.publicKey From 8b697bac5c5cdfcb15cecc516a8a81c815dba843 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Tue, 9 May 2023 12:18:56 +0200 Subject: [PATCH 104/153] enable eslint typescript strict --- backend/.eslintrc.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/.eslintrc.js b/backend/.eslintrc.js index d00b9199a..6a7d74b38 100644 --- a/backend/.eslintrc.js +++ b/backend/.eslintrc.js @@ -159,6 +159,7 @@ module.exports = { extends: [ 'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended-requiring-type-checking', + 'plugin:@typescript-eslint/strict', 'plugin:type-graphql/recommended', ], rules: { @@ -169,6 +170,8 @@ module.exports = { '@typescript-eslint/prefer-regexp-exec': 'off', // this should not run on ts files: https://github.com/import-js/eslint-plugin-import/issues/2215#issuecomment-911245486 'import/unambiguous': 'off', + // this is not compatible with typeorm, due to joined tables can be null, but are not defined as nullable + '@typescript-eslint/no-unnecessary-condition': 'off', }, parserOptions: { tsconfigRootDir: __dirname, From 49976b30b7d4d05ab22c33309173bfa8439f39c2 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Tue, 9 May 2023 12:20:28 +0200 Subject: [PATCH 105/153] fix problems --- backend/src/auth/JWT.ts | 2 +- backend/src/config/index.ts | 70 +++++++++---------- .../src/federation/client/GraphQLGetClient.ts | 2 +- backend/src/federation/validateCommunities.ts | 2 +- backend/src/graphql/directive/isAuthorized.ts | 4 +- backend/src/graphql/model/Balance.ts | 2 +- backend/src/graphql/model/Transaction.ts | 7 +- .../src/graphql/resolver/BalanceResolver.ts | 9 +-- .../resolver/ContributionResolver.test.ts | 8 +-- .../graphql/resolver/ContributionResolver.ts | 2 +- .../resolver/TransactionLinkResolver.test.ts | 4 +- .../resolver/TransactionLinkResolver.ts | 2 +- .../graphql/resolver/TransactionResolver.ts | 16 ++--- backend/src/graphql/resolver/UserResolver.ts | 18 ++--- .../resolver/util/findContributions.ts | 2 +- .../resolver/util/transactionLinkList.ts | 2 +- backend/src/index.ts | 2 +- backend/src/seeds/index.ts | 16 ++--- backend/src/server/createServer.ts | 8 ++- backend/src/server/plugins.ts | 6 +- backend/src/typeorm/DBVersion.ts | 4 +- backend/src/util/klicktipp.ts | 3 +- backend/src/util/utilities.ts | 2 +- backend/src/webhook/elopage.ts | 2 +- backend/test/helpers.ts | 4 +- 25 files changed, 99 insertions(+), 100 deletions(-) diff --git a/backend/src/auth/JWT.ts b/backend/src/auth/JWT.ts index 75a69cd0c..90e6947ba 100644 --- a/backend/src/auth/JWT.ts +++ b/backend/src/auth/JWT.ts @@ -8,7 +8,7 @@ import { CustomJwtPayload } from './CustomJwtPayload' export const decode = (token: string): CustomJwtPayload | null => { if (!token) throw new LogError('401 Unauthorized') try { - return verify(token, CONFIG.JWT_SECRET) + return verify(token, CONFIG.JWT_SECRET) as CustomJwtPayload } catch (err) { return null } diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index fc88011ea..ea3b5c19e 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -16,7 +16,7 @@ const constants = { 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: 'v15.2023-02-07', @@ -25,67 +25,67 @@ const constants = { } const server = { - PORT: process.env.PORT || 4000, - JWT_SECRET: process.env.JWT_SECRET || 'secret123', - JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m', + PORT: process.env.PORT ?? 4000, + JWT_SECRET: process.env.JWT_SECRET ?? 'secret123', + JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN ?? '10m', GRAPHIQL: process.env.GRAPHIQL === 'true' || false, - GDT_API_URL: process.env.GDT_API_URL || 'https://gdt.gradido.net', + GDT_API_URL: process.env.GDT_API_URL ?? 'https://gdt.gradido.net', 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', } const klicktipp = { KLICKTIPP: process.env.KLICKTIPP === 'true' || false, - KLICKTTIPP_API_URL: process.env.KLICKTIPP_API_URL || 'https://api.klicktipp.com', - KLICKTIPP_USER: process.env.KLICKTIPP_USER || 'gradido_test', - KLICKTIPP_PASSWORD: process.env.KLICKTIPP_PASSWORD || 'secret321', - KLICKTIPP_APIKEY_DE: process.env.KLICKTIPP_APIKEY_DE || 'SomeFakeKeyDE', - KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN || 'SomeFakeKeyEN', + KLICKTTIPP_API_URL: process.env.KLICKTIPP_API_URL ?? 'https://api.klicktipp.com', + KLICKTIPP_USER: process.env.KLICKTIPP_USER ?? 'gradido_test', + KLICKTIPP_PASSWORD: process.env.KLICKTIPP_PASSWORD ?? 'secret321', + KLICKTIPP_APIKEY_DE: process.env.KLICKTIPP_APIKEY_DE ?? 'SomeFakeKeyDE', + KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN ?? 'SomeFakeKeyEN', } 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_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_REDEEM_CONTRIBUTION_URL: - process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}', + process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL ?? 'http://localhost/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', + process.env.COMMUNITY_DESCRIPTION ?? 'Die lokale Entwicklungsumgebung von Gradido.', + COMMUNITY_SUPPORT_MAIL: process.env.COMMUNITY_SUPPORT_MAIL ?? 'support@supportmail.com', } const loginServer = { - LOGIN_APP_SECRET: process.env.LOGIN_APP_SECRET || '21ffbbc616fe', - LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY || 'a51ef8ac7ef1abf162fb7a65261acd7a', + LOGIN_APP_SECRET: process.env.LOGIN_APP_SECRET ?? '21ffbbc616fe', + LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY ?? 'a51ef8ac7ef1abf162fb7a65261acd7a', } const email = { 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', - EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || '', - EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'mailserver', + 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', + EMAIL_PASSWORD: process.env.EMAIL_PASSWORD ?? '', + EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL ?? 'mailserver', EMAIL_SMTP_PORT: Number(process.env.EMAIL_SMTP_PORT) || 1025, // 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}', + process.env.EMAIL_LINK_VERIFICATION ?? 'http://localhost/checkEmail/{optin}{code}', EMAIL_LINK_SETPASSWORD: - process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{optin}', + process.env.EMAIL_LINK_SETPASSWORD ?? 'http://localhost/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', + process.env.EMAIL_LINK_FORGOTPASSWORD ?? 'http://localhost/forgot-password', + EMAIL_LINK_OVERVIEW: process.env.EMAIL_LINK_OVERVIEW ?? 'http://localhost/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 @@ -98,14 +98,14 @@ const email = { const webhook = { // Elopage - WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret', + WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET ?? 'secret', } // This is needed by graphql-directive-auth process.env.APP_SECRET = server.JWT_SECRET // 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/backend/src/federation/client/GraphQLGetClient.ts b/backend/src/federation/client/GraphQLGetClient.ts index 2f5281532..e2d3e6ed3 100644 --- a/backend/src/federation/client/GraphQLGetClient.ts +++ b/backend/src/federation/client/GraphQLGetClient.ts @@ -1,7 +1,7 @@ import { GraphQLClient } from 'graphql-request' import { PatchedRequestInit } from 'graphql-request/dist/types' -type ClientInstance = { +interface ClientInstance { url: string // eslint-disable-next-line no-use-before-define client: GraphQLGetClient diff --git a/backend/src/federation/validateCommunities.ts b/backend/src/federation/validateCommunities.ts index b38f38ee9..073a8eded 100644 --- a/backend/src/federation/validateCommunities.ts +++ b/backend/src/federation/validateCommunities.ts @@ -54,7 +54,7 @@ export async function validateCommunities(): Promise { } else { logger.warn( `Federation: received not matching publicKey -> received: ${ - pubKey || 'null' + pubKey ?? 'null' }, expected: ${dbCom.publicKey.toString()} `, ) // DbCommunity.delete({ id: dbCom.id }) diff --git a/backend/src/graphql/directive/isAuthorized.ts b/backend/src/graphql/directive/isAuthorized.ts index b8595a2bd..8f4c6e22c 100644 --- a/backend/src/graphql/directive/isAuthorized.ts +++ b/backend/src/graphql/directive/isAuthorized.ts @@ -12,7 +12,7 @@ export const isAuthorized: AuthChecker = async ({ context }, rights) => context.role = ROLE_UNAUTHORIZED // unauthorized user // is rights an inalienable right? - if ((rights).reduce((acc, right) => acc && INALIENABLE_RIGHTS.includes(right), true)) + if ((rights as RIGHTS[]).reduce((acc, right) => acc && INALIENABLE_RIGHTS.includes(right), true)) return true // Do we have a token? @@ -43,7 +43,7 @@ export const isAuthorized: AuthChecker = async ({ context }, rights) => } // check for correct rights - const missingRights = (rights).filter((right) => !context.role?.hasRight(right)) + const missingRights = (rights as RIGHTS[]).filter((right) => !context.role?.hasRight(right)) if (missingRights.length !== 0) { throw new LogError('401 Unauthorized') } diff --git a/backend/src/graphql/model/Balance.ts b/backend/src/graphql/model/Balance.ts index 162ccc3c0..d992677b1 100644 --- a/backend/src/graphql/model/Balance.ts +++ b/backend/src/graphql/model/Balance.ts @@ -10,7 +10,7 @@ export class Balance { linkCount: number }) { this.balance = data.balance - this.balanceGDT = data.balanceGDT || null + this.balanceGDT = data.balanceGDT ?? null this.count = data.count this.linkCount = data.linkCount } diff --git a/backend/src/graphql/model/Transaction.ts b/backend/src/graphql/model/Transaction.ts index 3334dea84..bfc96089b 100644 --- a/backend/src/graphql/model/Transaction.ts +++ b/backend/src/graphql/model/Transaction.ts @@ -43,13 +43,12 @@ export class Transaction { this.memo = transaction.memo this.creationDate = transaction.creationDate this.linkedUser = linkedUser - this.linkedTransactionId = transaction.linkedTransactionId || null + this.linkedTransactionId = transaction.linkedTransactionId ?? null this.linkId = transaction.contribution ? transaction.contribution.contributionLinkId - : transaction.transactionLinkId || null + : transaction.transactionLinkId ?? null this.previousBalance = - (transaction.previousTransaction && - transaction.previousTransaction.balance.toDecimalPlaces(2, Decimal.ROUND_DOWN)) || + transaction.previousTransaction?.balance.toDecimalPlaces(2, Decimal.ROUND_DOWN) ?? new Decimal(0) } diff --git a/backend/src/graphql/resolver/BalanceResolver.ts b/backend/src/graphql/resolver/BalanceResolver.ts index deedb9dff..c8fdacdcf 100644 --- a/backend/src/graphql/resolver/BalanceResolver.ts +++ b/backend/src/graphql/resolver/BalanceResolver.ts @@ -70,7 +70,10 @@ export class BalanceResolver { now, ) logger.info( - `calculatedDecay(balance=${lastTransaction.balance}, balanceDate=${lastTransaction.balanceDate})=${calculatedDecay}`, + 'calculatedDecay', + lastTransaction.balance, + lastTransaction.balanceDate, + calculatedDecay, ) // The final balance is reduced by the link amount withheld @@ -96,9 +99,7 @@ export class BalanceResolver { count, linkCount, }) - logger.info( - `new Balance(balance=${balance}, balanceGDT=${balanceGDT}, count=${count}, linkCount=${linkCount}) = ${newBalance}`, - ) + logger.info('new Balance', balance, balanceGDT, count, linkCount, newBalance) return newBalance } diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 17c739fcb..d4c84b4f3 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -66,7 +66,7 @@ let testEnv: { query: ApolloServerTestClient['query'] con: Connection } -let creation: Contribution | void +let creation: Contribution | null let admin: User let pendingContribution: any let inProgressContribution: any @@ -2071,7 +2071,7 @@ describe('ContributionResolver', () => { mutate({ mutation: updateContribution, variables: { - contributionId: (adminContribution && adminContribution.id) || -1, + contributionId: adminContribution?.id ?? -1, amount: 100.0, memo: 'Test Test Test', creationDate: new Date().toString(), @@ -2565,8 +2565,8 @@ describe('ContributionResolver', () => { }) describe('confirm two creations one after the other quickly', () => { - let c1: Contribution | void - let c2: Contribution | void + let c1: Contribution | null + let c2: Contribution | null beforeAll(async () => { const now = new Date() diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 1884fecc4..d2203dac3 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -269,7 +269,7 @@ export class ContributionResolver { withDeleted: true, relations: ['user'], }) - if (!emailContact || !emailContact.user) { + if (!emailContact?.user) { throw new LogError('Could not find user', email) } if (emailContact.deletedAt || emailContact.user.deletedAt) { diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts index 04df0dc86..9d39a80ae 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts @@ -817,8 +817,8 @@ describe('TransactionLinkResolver', () => { const bibisTransaktionLinks = transactionLinks.filter( (transactionLink) => transactionLink.email === 'bibi@bloxberg.de', ) - for (let i = 0; i < bibisTransaktionLinks.length; i++) { - await transactionLinkFactory(testEnv, bibisTransaktionLinks[i]) + for (const bibisTransaktionLink of bibisTransaktionLinks) { + await transactionLinkFactory(testEnv, bibisTransaktionLink) } // admin: only now log in diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index ab322a50b..ca638d0be 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -146,7 +146,7 @@ export class TransactionLinkResolver { const transactionLink = await DbTransactionLink.findOneOrFail({ code }, { withDeleted: true }) const user = await DbUser.findOneOrFail({ id: transactionLink.userId }) let redeemedBy: User | null = null - if (transactionLink && transactionLink.redeemedBy) { + if (transactionLink?.redeemedBy) { redeemedBy = new User(await DbUser.findOneOrFail({ id: transactionLink.redeemedBy })) } return new TransactionLink(transactionLink, new User(user), redeemedBy) diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index bda108638..80c8be070 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -48,9 +48,7 @@ export const executeTransaction = async ( // acquire lock const releaseLock = await TRANSACTIONS_LOCK.acquire() try { - logger.info( - `executeTransaction(amount=${amount}, memo=${memo}, sender=${sender}, recipient=${recipient})...`, - ) + logger.info('executeTransaction', amount, memo, sender, recipient) if (sender.id === recipient.id) { throw new LogError('Sender and Recipient are the same', sender.id) @@ -119,10 +117,10 @@ export const executeTransaction = async ( // Save linked transaction id for send transactionSend.linkedTransactionId = transactionReceive.id await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend) - logger.debug(`send Transaction updated: ${transactionSend}`) + logger.debug('send Transaction updated', transactionSend) if (transactionLink) { - logger.info(`transactionLink: ${transactionLink}`) + logger.info('transactionLink', transactionLink) transactionLink.redeemedAt = receivedCallDate transactionLink.redeemedBy = recipient.id await queryRunner.manager.update( @@ -271,8 +269,8 @@ export class TransactionResolver { sumAmount.mul(-1), sumHoldAvailableAmount.mul(-1), sumHoldAvailableAmount.minus(sumAmount.toString()).mul(-1), - firstDate || now, - lastDate || now, + firstDate ?? now, + lastDate ?? now, self, (userTransactions.length && userTransactions[0].balance) || new Decimal(0), ), @@ -325,9 +323,7 @@ export class TransactionResolver { } await executeTransaction(amount, memo, senderUser, recipientUser) - logger.info( - `successful executeTransaction(amount=${amount}, memo=${memo}, senderUser=${senderUser}, recipientUser=${recipientUser})`, - ) + logger.info('successful executeTransaction', amount, memo, senderUser, recipientUser) return true } } diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 23b3c6aa4..e16de98b9 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -94,7 +94,7 @@ const newEmailContact = (email: string, userId: number): DbUserContact => { emailContact.emailChecked = false emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER emailContact.emailVerificationCode = random(64) - logger.debug(`newEmailContact...successful: ${emailContact}`) + logger.debug('newEmailContact...successful', emailContact) return emailContact } @@ -225,7 +225,7 @@ export class UserResolver { email = email.trim().toLowerCase() if (await checkEmailExists(email)) { const foundUser = await findUserByEmail(email) - logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`) + logger.info('DbUser.findOne', email, foundUser) if (foundUser) { // ATTENTION: this logger-message will be exactly expected during tests, next line @@ -275,7 +275,7 @@ export class UserResolver { dbUser.firstName = firstName dbUser.lastName = lastName dbUser.language = language - dbUser.publisherId = publisherId || 0 + dbUser.publisherId = publisherId ?? 0 dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD logger.debug('new dbUser', dbUser) if (redeemCode) { @@ -382,7 +382,7 @@ export class UserResolver { throw new LogError('Unable to save email verification code', user.emailContact) }) - logger.info(`optInCode for ${email}=${user.emailContact}`) + logger.info('optInCode for', email, user.emailContact) void sendResetPasswordEmail({ firstName: user.firstName, @@ -486,7 +486,7 @@ export class UserResolver { async queryOptIn(@Arg('optIn') optIn: string): Promise { logger.info(`queryOptIn(${optIn})...`) const userContact = await DbUserContact.findOneOrFail({ emailVerificationCode: optIn }) - logger.debug(`found optInCode=${userContact}`) + logger.debug('found optInCode', userContact) // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) { throw new LogError( @@ -586,7 +586,7 @@ export class UserResolver { logger.info(`hasElopage()...`) const userEntity = getUser(context) const elopageBuys = hasElopageBuys(userEntity.emailContact.email) - logger.debug(`has ElopageBuys = ${elopageBuys}`) + logger.debug('has ElopageBuys', elopageBuys) return elopageBuys } @@ -643,7 +643,7 @@ export class UserResolver { return 'user.' + fieldName }), searchText, - filters || null, + filters ?? null, currentPage, pageSize, ) @@ -709,14 +709,14 @@ export class UserResolver { // change isAdmin switch (user.isAdmin) { case null: - if (isAdmin === true) { + if (isAdmin) { user.isAdmin = new Date() } else { throw new LogError('User is already an usual user') } break default: - if (isAdmin === false) { + if (!isAdmin) { user.isAdmin = null } else { throw new LogError('User is already admin') diff --git a/backend/src/graphql/resolver/util/findContributions.ts b/backend/src/graphql/resolver/util/findContributions.ts index a08631e2c..28984d5b1 100644 --- a/backend/src/graphql/resolver/util/findContributions.ts +++ b/backend/src/graphql/resolver/util/findContributions.ts @@ -24,7 +24,7 @@ export const findContributions = async ( } return DbContribution.findAndCount({ where: { - ...(statusFilter && statusFilter.length && { contributionStatus: In(statusFilter) }), + ...(statusFilter?.length && { contributionStatus: In(statusFilter) }), ...(userId && { userId }), }, withDeleted, diff --git a/backend/src/graphql/resolver/util/transactionLinkList.ts b/backend/src/graphql/resolver/util/transactionLinkList.ts index 0dba5a400..ee79216c8 100644 --- a/backend/src/graphql/resolver/util/transactionLinkList.ts +++ b/backend/src/graphql/resolver/util/transactionLinkList.ts @@ -14,7 +14,7 @@ export async function transactionLinkList( filters: TransactionLinkFilters | null, user: DbUser, ): Promise { - const { withDeleted, withExpired, withRedeemed } = filters || { + const { withDeleted, withExpired, withRedeemed } = filters ?? { withDeleted: false, withExpired: false, withRedeemed: false, diff --git a/backend/src/index.ts b/backend/src/index.ts index 86f78326d..6e0191155 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -13,7 +13,7 @@ async function main() { console.log(`GraphIQL available at http://localhost:${CONFIG.PORT}`) } }) - void startValidateCommunities(Number(CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER)) + startValidateCommunities(Number(CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER)) } main().catch((e) => { diff --git a/backend/src/seeds/index.ts b/backend/src/seeds/index.ts index c2533765e..fab81eb95 100644 --- a/backend/src/seeds/index.ts +++ b/backend/src/seeds/index.ts @@ -31,8 +31,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) } } @@ -73,20 +73,20 @@ const run = async () => { logger.info('##seed## seeding all random users successful...') // create GDD - for (let i = 0; i < creations.length; i++) { - await creationFactory(seedClient, creations[i]) + for (const creation of creations) { + await creationFactory(seedClient, creation) } logger.info('##seed## seeding all creations successful...') // create Transaction Links - for (let i = 0; i < transactionLinks.length; i++) { - await transactionLinkFactory(seedClient, transactionLinks[i]) + for (const transactionLink of transactionLinks) { + await transactionLinkFactory(seedClient, transactionLink) } logger.info('##seed## seeding all transactionLinks successful...') // create Contribution Links - for (let i = 0; i < contributionLinks.length; i++) { - await contributionLinkFactory(seedClient, contributionLinks[i]) + for (const contributionLink of contributionLinks) { + await contributionLinkFactory(seedClient, contributionLink) } logger.info('##seed## seeding all contributionLinks successful...') diff --git a/backend/src/server/createServer.ts b/backend/src/server/createServer.ts index 777d6dfbe..d813c541e 100644 --- a/backend/src/server/createServer.ts +++ b/backend/src/server/createServer.ts @@ -21,7 +21,11 @@ import { plugins } from './plugins' // TODO implement // import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity"; -type ServerDef = { apollo: ApolloServer; app: Express; con: Connection } +interface ServerDef { + apollo: ApolloServer + app: Express + con: Connection +} export const createServer = async ( // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -34,7 +38,7 @@ export const createServer = async ( // open mysql connection const con = await connection() - if (!con || !con.isConnected) { + 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/server/plugins.ts b/backend/src/server/plugins.ts index 1da062b83..3e0fc50e1 100644 --- a/backend/src/server/plugins.ts +++ b/backend/src/server/plugins.ts @@ -12,7 +12,7 @@ const setHeadersPlugin = { return { willSendResponse(requestContext: any) { const { setHeaders = [] } = requestContext.context - setHeaders.forEach(({ key, value }: { [key: string]: string }) => { + setHeaders.forEach(({ key, value }: Record) => { if (requestContext.response.http.headers.get(key)) { requestContext.response.http.headers.set(key, value) } else { @@ -27,8 +27,8 @@ const setHeadersPlugin = { const filterVariables = (variables: any) => { const vars = clonedeep(variables) - if (vars && vars.password) vars.password = '***' - if (vars && vars.passwordNew) vars.passwordNew = '***' + if (vars?.password) vars.password = '***' + if (vars?.passwordNew) vars.passwordNew = '***' return vars } diff --git a/backend/src/typeorm/DBVersion.ts b/backend/src/typeorm/DBVersion.ts index c4c8d6c78..f465069d3 100644 --- a/backend/src/typeorm/DBVersion.ts +++ b/backend/src/typeorm/DBVersion.ts @@ -14,10 +14,10 @@ const getDBVersion = async (): Promise => { const checkDBVersion = async (DB_VERSION: string): Promise => { const dbVersion = await getDBVersion() - if (!dbVersion || dbVersion.indexOf(DB_VERSION) === -1) { + if (!dbVersion?.includes(DB_VERSION)) { logger.error( `Wrong database version detected - the backend requires '${DB_VERSION}' but found '${ - dbVersion || 'None' + dbVersion ?? 'None' }`, ) return false diff --git a/backend/src/util/klicktipp.ts b/backend/src/util/klicktipp.ts index e34a9c384..a0ba3c0f7 100644 --- a/backend/src/util/klicktipp.ts +++ b/backend/src/util/klicktipp.ts @@ -11,8 +11,7 @@ export async function retrieveNotRegisteredEmails(): Promise { } const users = await User.find({ relations: ['emailContact'] }) const notRegisteredUser = [] - for (let i = 0; i < users.length; i++) { - const user = users[i] + for (const user of users) { try { await getKlickTippUser(user.emailContact.email) } catch (err) { diff --git a/backend/src/util/utilities.ts b/backend/src/util/utilities.ts index 2cf53f1e4..e9935bbd4 100644 --- a/backend/src/util/utilities.ts +++ b/backend/src/util/utilities.ts @@ -1,7 +1,7 @@ import { Decimal } from 'decimal.js-light' import i18n from 'i18n' -export const objectValuesToArray = (obj: { [x: string]: string }): Array => { +export const objectValuesToArray = (obj: Record): string[] => { return Object.keys(obj).map(function (key) { return obj[key] }) diff --git a/backend/src/webhook/elopage.ts b/backend/src/webhook/elopage.ts index f386b6e99..7a779fadd 100644 --- a/backend/src/webhook/elopage.ts +++ b/backend/src/webhook/elopage.ts @@ -146,7 +146,7 @@ export const elopageWebhook = async (req: any, res: any): Promise => { email, firstName, lastName, - publisherId: loginElopageBuy.publisherId || 0, // This seemed to be the default value if not set + publisherId: loginElopageBuy.publisherId ?? 0, // This seemed to be the default value if not set }) } catch (error) { // eslint-disable-next-line no-console diff --git a/backend/test/helpers.ts b/backend/test/helpers.ts index d42db959f..7f55b3c70 100644 --- a/backend/test/helpers.ts +++ b/backend/test/helpers.ts @@ -22,8 +22,8 @@ const context = { export const cleanDB = async () => { // this only works as lond 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) } } From b7ce0e277ccc04d0c673fba87d3aa6df09dd4eb9 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 9 May 2023 13:58:53 +0200 Subject: [PATCH 106/153] fix variable name --- backend/src/graphql/resolver/UserResolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 3674f6602..7198a3bdc 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -538,7 +538,7 @@ export class UserResolver { if (aliasInUse.length !== 0) { throw new LogError('Alias already in use', alias) } - userEntity.alias = alias + user.alias = alias } if (language) { From 1f90f9e63b4aa803cf3ff2095f4dfd52a11ca6a4 Mon Sep 17 00:00:00 2001 From: elweyn Date: Tue, 9 May 2023 18:05:37 +0200 Subject: [PATCH 107/153] Remove unused code. --- backend/src/graphql/resolver/UserResolver.test.ts | 13 +++++-------- backend/src/middleware/klicktippMiddleware.ts | 13 ------------- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index d083eba00..4ae042a1e 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -20,6 +20,7 @@ import { ContributionLink } from '@model/ContributionLink' import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers' import { logger, i18n as localization } from '@test/testSetup' +import { subscribe } from '@/apis/KlicktippController' import { CONFIG } from '@/config' import { sendAccountActivationEmail, @@ -61,8 +62,6 @@ import { stephenHawking } from '@/seeds/users/stephen-hawking' import { printTimeDuration } from '@/util/time' import { objectValuesToArray } from '@/util/utilities' -// import { subscribe } from '@/apis/KlicktippController' - jest.mock('@/emails/sendEmailVariants', () => { const originalModule = jest.requireActual('@/emails/sendEmailVariants') return { @@ -556,16 +555,14 @@ describe('UserResolver', () => { expect(newUser.password.toString()).toEqual(encryptedPass.toString()) }) - /* it('calls the klicktipp API', () => { expect(subscribe).toBeCalledWith( - user[0].email, - user[0].language, - user[0].firstName, - user[0].lastName, + newUser.emailContact.email, + newUser.language, + newUser.firstName, + newUser.lastName, ) }) - */ it('returns true', () => { expect(result).toBeTruthy() diff --git a/backend/src/middleware/klicktippMiddleware.ts b/backend/src/middleware/klicktippMiddleware.ts index c988c6d7a..038bd3dd3 100644 --- a/backend/src/middleware/klicktippMiddleware.ts +++ b/backend/src/middleware/klicktippMiddleware.ts @@ -10,19 +10,6 @@ import { KlickTipp } from '@model/KlickTipp' import { getKlickTippUser } from '@/apis/KlicktippController' import { klickTippLogger as logger } from '@/server/logger' -// export const klicktippRegistrationMiddleware: MiddlewareFn = async ( -// // Only for demo -// /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -// { root, args, context, info }, -// next, -// ) => { -// // Do Something here before resolver is called -// const result = await next() -// // Do Something here after resolver is completed -// await subscribe(result.email, result.language, result.firstName, result.lastName) -// return result -// } - export const klicktippNewsletterStateMiddleware: MiddlewareFn = async ( /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ { root, args, context, info }, From 864beb7914b7ab93fb0aa6aa6be89f60a7667b2e Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Tue, 9 May 2023 18:23:21 +0200 Subject: [PATCH 108/153] linting --- .../migrations/0066-x-community-sendcoins-transactions_table.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/database/migrations/0066-x-community-sendcoins-transactions_table.ts b/database/migrations/0066-x-community-sendcoins-transactions_table.ts index 10693bdba..62dd95f62 100644 --- a/database/migrations/0066-x-community-sendcoins-transactions_table.ts +++ b/database/migrations/0066-x-community-sendcoins-transactions_table.ts @@ -76,7 +76,6 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis await queryFn( `UPDATE transactions t, users u SET t.linked_user_gradido_id = u.gradido_id, t.linked_user_name = concat(u.first_name, ' ', u.last_name) WHERE t.linked_user_id = u.id and t.linked_user_id is null and t.linked_user_gradido_id is null;`, ) - } /* eslint-disable @typescript-eslint/no-empty-function */ From bcb08fc0fb222b38e59964f4014e79cb0be26a43 Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Tue, 9 May 2023 20:06:50 +0200 Subject: [PATCH 109/153] correct migration after test with production data --- .../migrations/0066-x-community-sendcoins-transactions_table.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/0066-x-community-sendcoins-transactions_table.ts b/database/migrations/0066-x-community-sendcoins-transactions_table.ts index 62dd95f62..65c21e0b5 100644 --- a/database/migrations/0066-x-community-sendcoins-transactions_table.ts +++ b/database/migrations/0066-x-community-sendcoins-transactions_table.ts @@ -74,7 +74,7 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis ) */ await queryFn( - `UPDATE transactions t, users u SET t.linked_user_gradido_id = u.gradido_id, t.linked_user_name = concat(u.first_name, ' ', u.last_name) WHERE t.linked_user_id = u.id and t.linked_user_id is null and t.linked_user_gradido_id is null;`, + `UPDATE transactions t, users u SET t.linked_user_gradido_id = u.gradido_id, t.linked_user_name = concat(u.first_name, ' ', u.last_name) WHERE t.linked_user_id = u.id and t.linked_user_gradido_id is null;`, ) } From 15f0d062ee45a2d96c0e0135cde2083a1a2f383d Mon Sep 17 00:00:00 2001 From: Claus-Peter Huebner Date: Wed, 10 May 2023 00:29:33 +0200 Subject: [PATCH 110/153] remove commented code --- .../0066-x-community-sendcoins-transactions_table.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/database/migrations/0066-x-community-sendcoins-transactions_table.ts b/database/migrations/0066-x-community-sendcoins-transactions_table.ts index 65c21e0b5..2a3b3973a 100644 --- a/database/migrations/0066-x-community-sendcoins-transactions_table.ts +++ b/database/migrations/0066-x-community-sendcoins-transactions_table.ts @@ -68,11 +68,6 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis await queryFn( 'ALTER TABLE `transactions` MODIFY COLUMN `user_gradido_id` char(36) NOT NULL AFTER `user_id`;', ) - /* - await queryFn( - 'ALTER TABLE `transactions` MODIFY COLUMN `user_name` varchar(512) COLLATE utf8mb4_unicode_ci NOT NULL AFTER `user_community_uuid`;', - ) - */ await queryFn( `UPDATE transactions t, users u SET t.linked_user_gradido_id = u.gradido_id, t.linked_user_name = concat(u.first_name, ' ', u.last_name) WHERE t.linked_user_id = u.id and t.linked_user_gradido_id is null;`, ) From d69afa7e9f2c4b57ae8c1153928172c441c2245a Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 10 May 2023 12:13:51 +0200 Subject: [PATCH 111/153] eslint security --- backend/.eslintrc.js | 1 + backend/package.json | 1 + backend/yarn.lock | 21 ++++++++++++++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/backend/.eslintrc.js b/backend/.eslintrc.js index 6a7d74b38..7f2ecd578 100644 --- a/backend/.eslintrc.js +++ b/backend/.eslintrc.js @@ -12,6 +12,7 @@ module.exports = { 'plugin:prettier/recommended', 'plugin:import/recommended', 'plugin:import/typescript', + 'plugin:security/recommended', ], settings: { 'import/parsers': { diff --git a/backend/package.json b/backend/package.json index c5e0df3c5..c6e852841 100644 --- a/backend/package.json +++ b/backend/package.json @@ -68,6 +68,7 @@ "eslint-plugin-n": "^15.7.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-security": "^1.7.1", "eslint-plugin-type-graphql": "^1.0.0", "faker": "^5.5.3", "graphql-tag": "^2.12.6", diff --git a/backend/yarn.lock b/backend/yarn.lock index 1bc8c64fd..4a283e482 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -3005,6 +3005,13 @@ eslint-plugin-promise@^6.1.1: resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz#269a3e2772f62875661220631bd4dafcb4083816" integrity sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig== +eslint-plugin-security@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-security/-/eslint-plugin-security-1.7.1.tgz#0e9c4a471f6e4d3ca16413c7a4a51f3966ba16e4" + integrity sha512-sMStceig8AFglhhT2LqlU5r+/fn9OwsA72O5bBuQVTssPCdQAOQzL+oMn/ZcpeUY6KcNfLJArgcrsSULNjYYdQ== + dependencies: + safe-regex "^2.1.1" + eslint-plugin-type-graphql@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eslint-plugin-type-graphql/-/eslint-plugin-type-graphql-1.0.0.tgz#d348560ed628d6ca1dfcea35a02891432daafe6b" @@ -3649,7 +3656,7 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0: integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== "gradido-database@file:../database": - version "1.19.1" + version "1.20.0" dependencies: "@types/uuid" "^8.3.4" cross-env "^7.0.3" @@ -6140,6 +6147,11 @@ reflect-metadata@^0.1.13: resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== +regexp-tree@~0.1.1: + version "0.1.27" + resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" + integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== + regexp.prototype.flags@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" @@ -6279,6 +6291,13 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" +safe-regex@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-2.1.1.tgz#f7128f00d056e2fe5c11e81a1324dd974aadced2" + integrity sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A== + dependencies: + regexp-tree "~0.1.1" + "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" From 2545f1342e9a6147923bab9d8b7664a9c66ee2ee Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 10 May 2023 12:14:13 +0200 Subject: [PATCH 112/153] fix security issue --- backend/src/seeds/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/src/seeds/index.ts b/backend/src/seeds/index.ts index fab81eb95..77fa51990 100644 --- a/backend/src/seeds/index.ts +++ b/backend/src/seeds/index.ts @@ -54,9 +54,8 @@ const run = async () => { logger.info('##seed## clean database successful...') // seed the standard users - for (let i = 0; i < users.length; i++) { - const dbUser = await userFactory(seedClient, users[i]) - logger.info(`##seed## seed standard users[ ${i} ]= ${JSON.stringify(dbUser, null, 2)}`) + for (const user of users) { + await userFactory(seedClient, user) } logger.info('##seed## seeding all standard users successful...') From a319bc2f034afa0d29dcc754cc5efbfc0afc8e9c Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 10 May 2023 12:14:47 +0200 Subject: [PATCH 113/153] refactor utilies function, explicitly ignore security warning --- backend/src/util/utilities.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/backend/src/util/utilities.ts b/backend/src/util/utilities.ts index e9935bbd4..d5da55aed 100644 --- a/backend/src/util/utilities.ts +++ b/backend/src/util/utilities.ts @@ -1,11 +1,9 @@ import { Decimal } from 'decimal.js-light' import i18n from 'i18n' -export const objectValuesToArray = (obj: Record): string[] => { - return Object.keys(obj).map(function (key) { - return obj[key] - }) -} +export const objectValuesToArray = (obj: Record): string[] => + // eslint-disable-next-line security/detect-object-injection + Object.keys(obj).map((key) => obj[key]) export const decimalSeparatorByLanguage = (a: Decimal, language: string): string => { const rememberLocaleToRestore = i18n.getLocale() From e01ef9ad100f82f67d3448f34d1d8f050aad238b Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 10 May 2023 12:15:12 +0200 Subject: [PATCH 114/153] ignore security warning where not applicable or unfixable for now --- backend/src/graphql/resolver/TransactionLinkResolver.test.ts | 1 + backend/src/graphql/resolver/util/creations.ts | 4 ++++ backend/src/server/logger.ts | 1 + backend/src/webhook/elopage.ts | 1 + 4 files changed, 7 insertions(+) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts index 9d39a80ae..3c6ba31ab 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.test.ts @@ -1040,6 +1040,7 @@ describe('TransactionLinkResolver', () => { }) it('returns a string that ends with the hex value of date', () => { + // eslint-disable-next-line security/detect-non-literal-regexp const regexp = new RegExp(date.getTime().toString(16) + '$') expect(transactionLinkCode(date)).toEqual(expect.stringMatching(regexp)) }) diff --git a/backend/src/graphql/resolver/util/creations.ts b/backend/src/graphql/resolver/util/creations.ts index 1c0c0735e..d6f0e9af4 100644 --- a/backend/src/graphql/resolver/util/creations.ts +++ b/backend/src/graphql/resolver/util/creations.ts @@ -29,10 +29,12 @@ export const validateContribution = ( throw new LogError('No information for available creations for the given date', creationDate) } + // eslint-disable-next-line security/detect-object-injection if (amount.greaterThan(creations[index].toString())) { throw new LogError( 'The amount to be created exceeds the amount still available for this month', amount, + // eslint-disable-next-line security/detect-object-injection creations[index], ) } @@ -151,6 +153,7 @@ export const updateCreations = ( if (index < 0) { throw new LogError('You cannot create GDD for a month older than the last three months') } + // eslint-disable-next-line security/detect-object-injection creations[index] = creations[index].plus(contribution.amount.toString()) return creations } @@ -169,6 +172,7 @@ export const getOpenCreations = async ( return { month: date.getMonth(), year: date.getFullYear(), + // eslint-disable-next-line security/detect-object-injection amount: creations[index], } }) diff --git a/backend/src/server/logger.ts b/backend/src/server/logger.ts index d1edbd8fb..0f146b7f7 100644 --- a/backend/src/server/logger.ts +++ b/backend/src/server/logger.ts @@ -7,6 +7,7 @@ import { configure, getLogger } from 'log4js' import { CONFIG } from '@/config' +// eslint-disable-next-line security/detect-non-literal-fs-filename const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8')) options.categories.backend.level = CONFIG.LOG_LEVEL diff --git a/backend/src/webhook/elopage.ts b/backend/src/webhook/elopage.ts index 7a779fadd..07e7d4ecf 100644 --- a/backend/src/webhook/elopage.ts +++ b/backend/src/webhook/elopage.ts @@ -115,6 +115,7 @@ export const elopageWebhook = async (req: any, res: any): Promise => { ) { const email = loginElopageBuy.payerEmail + // eslint-disable-next-line security/detect-unsafe-regex const VALIDATE_EMAIL = /^[a-zA-Z0-9.!#$%&?*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/ const VALIDATE_NAME = /^<>&;]{2,}$/ From c41f9bccea3c9c83bffa7dbbdd6133c642fb79b7 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 10 May 2023 12:51:34 +0200 Subject: [PATCH 115/153] Add mock for KlicktippController. --- backend/src/graphql/resolver/UserResolver.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 4ae042a1e..ddfcf173a 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -75,15 +75,13 @@ jest.mock('@/emails/sendEmailVariants', () => { } }) -/* - jest.mock('@/apis/KlicktippController', () => { return { __esModule: true, subscribe: jest.fn(), + getKlickTippUser: jest.fn(), } }) -*/ let admin: User let user: User From 4ac1988b34d3ca4361aef4e1938f7e8bab9c6fbd Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 10 May 2023 13:03:02 +0200 Subject: [PATCH 116/153] lint eslint comments --- backend/.eslintrc.js | 3 +++ backend/package.json | 1 + backend/yarn.lock | 10 +++++++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/.eslintrc.js b/backend/.eslintrc.js index 7f2ecd578..158d1a0bb 100644 --- a/backend/.eslintrc.js +++ b/backend/.eslintrc.js @@ -13,6 +13,7 @@ module.exports = { 'plugin:import/recommended', 'plugin:import/typescript', 'plugin:security/recommended', + 'plugin:@eslint-community/eslint-comments/recommended', ], settings: { 'import/parsers': { @@ -152,6 +153,8 @@ module.exports = { 'promise/valid-params': 'warn', 'promise/prefer-await-to-callbacks': 'error', 'promise/no-multiple-resolved': 'error', + // eslint comments + '@eslint-community/eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }], }, overrides: [ // only for ts files diff --git a/backend/package.json b/backend/package.json index c6e852841..8a8d14e00 100644 --- a/backend/package.json +++ b/backend/package.json @@ -46,6 +46,7 @@ "uuid": "^8.3.2" }, "devDependencies": { + "@eslint-community/eslint-plugin-eslint-comments": "^3.2.1", "@types/email-templates": "^10.0.1", "@types/express": "^4.17.12", "@types/faker": "^5.5.9", diff --git a/backend/yarn.lock b/backend/yarn.lock index 4a283e482..237a265e3 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -382,6 +382,14 @@ dependencies: "@cspotcode/source-map-consumer" "0.8.0" +"@eslint-community/eslint-plugin-eslint-comments@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.1.tgz#3c65061e27f155eae3744c3b30c5a8253a959040" + integrity sha512-/HZbjIGaVO2zLlWX3gRgiHmKRVvvqrC0zVu3eXnIj1ORxoyfGSj50l0PfDfqihyZAqrDYzSMdJesXzFjvAoiLQ== + dependencies: + escape-string-regexp "^1.0.5" + ignore "^5.2.4" + "@eslint-community/eslint-utils@^4.2.0": version "4.2.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz#a831e6e468b4b2b5ae42bf658bea015bf10bc518" @@ -3984,7 +3992,7 @@ ignore@^5.1.1: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== -ignore@^5.2.0: +ignore@^5.2.0, ignore@^5.2.4: version "5.2.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== From 7483cef63419f2d9bab099d1ebaa4f95b0ff2298 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 10 May 2023 13:03:13 +0200 Subject: [PATCH 117/153] ignore coverage folder for linting --- backend/.eslintignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/.eslintignore b/backend/.eslintignore index f6b255e92..1ae86fe5e 100644 --- a/backend/.eslintignore +++ b/backend/.eslintignore @@ -1,3 +1,4 @@ node_modules **/*.min.js -build \ No newline at end of file +build +coverage \ No newline at end of file From ab5aac5c6ba9bb26998d0b5f4748690b4f6af20e Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 10 May 2023 13:03:23 +0200 Subject: [PATCH 118/153] remove duplicate ignore --- backend/src/apis/HttpRequest.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/src/apis/HttpRequest.ts b/backend/src/apis/HttpRequest.ts index f40d577bd..27578463a 100644 --- a/backend/src/apis/HttpRequest.ts +++ b/backend/src/apis/HttpRequest.ts @@ -7,7 +7,6 @@ import axios from 'axios' import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' -// eslint-disable-next-line @typescript-eslint/no-explicit-any export const apiPost = async (url: string, payload: unknown): Promise => { logger.trace('POST', url, payload) try { @@ -25,7 +24,6 @@ export const apiPost = async (url: string, payload: unknown): Promise => { } } -// eslint-disable-next-line @typescript-eslint/no-explicit-any export const apiGet = async (url: string): Promise => { logger.trace('GET: url=' + url) try { From ac89b845edb6b9cdb6ff35690336051581c8ad89 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 10 May 2023 13:08:31 +0200 Subject: [PATCH 119/153] enable and document optional rules --- backend/.eslintrc.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/.eslintrc.js b/backend/.eslintrc.js index 158d1a0bb..798bef1e6 100644 --- a/backend/.eslintrc.js +++ b/backend/.eslintrc.js @@ -155,6 +155,9 @@ module.exports = { 'promise/no-multiple-resolved': 'error', // eslint comments '@eslint-community/eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }], + '@eslint-community/eslint-comments/no-restricted-disable': 'error', + '@eslint-community/eslint-comments/no-use': 'off', + '@eslint-community/eslint-comments/require-description': 'off', }, overrides: [ // only for ts files From da23956c0ad0a0f1c0648e74f5ab5b4fa1d296a4 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 10 May 2023 13:09:33 +0200 Subject: [PATCH 120/153] Upgrade to 86% coverage in backend. --- backend/jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/jest.config.js b/backend/jest.config.js index ca12668fa..3b251916a 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -7,7 +7,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], coverageThreshold: { global: { - lines: 85, + lines: 86, }, }, setupFiles: ['/test/testSetup.ts'], From a82f0cb2845d0d2649d8177b2e41265d52294a88 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 10 May 2023 17:37:01 +0200 Subject: [PATCH 121/153] separate validation function for alias, tested --- .../src/graphql/resolver/UserResolver.test.ts | 101 -------------- backend/src/graphql/resolver/UserResolver.ts | 15 +-- .../resolver/util/validateAlias.test.ts | 125 ++++++++++++++++++ .../graphql/resolver/util/validateAlias.ts | 36 +++++ 4 files changed, 163 insertions(+), 114 deletions(-) create mode 100644 backend/src/graphql/resolver/util/validateAlias.test.ts create mode 100644 backend/src/graphql/resolver/util/validateAlias.ts diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 8ebe3e7af..5e39ae2ff 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -1208,107 +1208,6 @@ describe('UserResolver', () => { jest.clearAllMocks() }) - describe('too short', () => { - it('throws and logs an error', async () => { - await expect( - mutate({ - mutation: updateUserInfos, - variables: { - alias: 'bibi', - }, - }), - ).resolves.toMatchObject({ - errors: [new GraphQLError('Given alias is too short')], - data: null, - }) - expect(logger.error).toBeCalledWith('Given alias is too short', 'bibi') - }) - }) - - describe('too long', () => { - it('throws and logs an error', async () => { - await expect( - mutate({ - mutation: updateUserInfos, - variables: { - alias: 'bibis_alias_far_too_long', - }, - }), - ).resolves.toMatchObject({ - errors: [new GraphQLError('Given alias is too long')], - data: null, - }) - expect(logger.error).toBeCalledWith( - 'Given alias is too long', - 'bibis_alias_far_too_long', - ) - }) - }) - - describe('invalid characters', () => { - it('throws and logs an error', async () => { - await expect( - mutate({ - mutation: updateUserInfos, - variables: { - alias: 'no+äöllll', - }, - }), - ).resolves.toMatchObject({ - errors: [new GraphQLError('Invalid characters in alias')], - data: null, - }) - expect(logger.error).toBeCalledWith('Invalid characters in alias', 'no+äöllll') - }) - }) - - describe('alias exists', () => { - let peter: User - beforeAll(async () => { - peter = await userFactory(testEnv, peterLustig) - await mutate({ - mutation: login, - variables: { - email: 'peter@lustig.de', - password: 'Aa12345_', - }, - }) - await mutate({ - mutation: updateUserInfos, - variables: { - alias: 'bibiBloxberg', - }, - }) - await mutate({ - mutation: login, - variables: { - email: 'bibi@bloxberg.de', - password: 'Aa12345_', - }, - }) - }) - - afterAll(async () => { - const user = await User.findOne({ id: peter.id }) - await user.remove() - }) - - it('throws and logs an error', async () => { - await expect( - mutate({ - mutation: updateUserInfos, - variables: { - alias: 'bibiBloxberg', - }, - }), - ).resolves.toMatchObject({ - errors: [new GraphQLError('Alias already in use')], - data: null, - }) - expect(logger.error).toBeCalledWith('Alias already in use', 'bibiBloxberg') - }) - }) - describe('valid alias', () => { it('updates the user in DB', async () => { await mutate({ diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 7198a3bdc..6b4844154 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -73,6 +73,7 @@ import { getTimeDurationObject, printTimeDuration } from '@/util/time' 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') @@ -525,19 +526,7 @@ export class UserResolver { } if (alias) { - if (alias.length < 5) { - throw new LogError('Given alias is too short', alias) - } - if (alias.length > 20) { - throw new LogError('Given alias is too long', alias) - } - if (!alias.match(/^[0-9A-Za-z]([_-]?[A-Za-z0-9])+$/)) { - throw new LogError('Invalid characters in alias', alias) - } - const aliasInUse = await DbUser.find({ alias }) - if (aliasInUse.length !== 0) { - throw new LogError('Alias already in use', alias) - } + await validateAlias(alias) user.alias = alias } diff --git a/backend/src/graphql/resolver/util/validateAlias.test.ts b/backend/src/graphql/resolver/util/validateAlias.test.ts new file mode 100644 index 000000000..733a09ffe --- /dev/null +++ b/backend/src/graphql/resolver/util/validateAlias.test.ts @@ -0,0 +1,125 @@ +import { Connection } from '@dbTools/typeorm' +import { User } from '@entity/User' +import { ApolloServerTestClient } from 'apollo-server-testing' + +import { testEnvironment, cleanDB } from '@test/helpers' +import { logger, i18n as localization } from '@test/testSetup' + +import { userFactory } from '@/seeds/factory/user' +import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' + +import { validateAlias } from './validateAlias' + +let con: Connection +let testEnv: { + mutate: ApolloServerTestClient['mutate'] + query: ApolloServerTestClient['query'] + con: Connection +} + +beforeAll(async () => { + testEnv = await testEnvironment(logger, localization) + con = testEnv.con + await cleanDB() +}) + +afterAll(async () => { + await cleanDB() + await con.close() +}) + +describe('validate alias', () => { + beforeAll(() => { + jest.clearAllMocks() + }) + + describe('alias too short', () => { + it('throws and logs an error', async () => { + await expect(validateAlias('Bi')).rejects.toEqual(new Error('Given alias is too short')) + expect(logger.error).toBeCalledWith('Given alias is too short', 'Bi') + }) + }) + + describe('alias too long', () => { + it('throws and logs an error', async () => { + await expect(validateAlias('BibiBloxbergHexHexHex')).rejects.toEqual( + new Error('Given alias is too long'), + ) + expect(logger.error).toBeCalledWith('Given alias is too long', 'BibiBloxbergHexHexHex') + }) + }) + + describe('alias contains invalid characters', () => { + it('throws and logs an error', async () => { + await expect(validateAlias('Bibi.Bloxberg')).rejects.toEqual( + new Error('Invalid characters in alias'), + ) + expect(logger.error).toBeCalledWith('Invalid characters in alias', 'Bibi.Bloxberg') + }) + }) + + describe('alias is a reserved word', () => { + it('throws and logs an error', async () => { + await expect(validateAlias('admin')).rejects.toEqual(new Error('Alias is not allowed')) + expect(logger.error).toBeCalledWith('Alias is not allowed', 'admin') + }) + }) + + describe('alias is a reserved word with uppercase characters', () => { + it('throws and logs an error', async () => { + await expect(validateAlias('Admin')).rejects.toEqual(new Error('Alias is not allowed')) + expect(logger.error).toBeCalledWith('Alias is not allowed', 'Admin') + }) + }) + + describe('hyphens and underscore', () => { + describe('alias starts with underscore', () => { + it('throws and logs an error', async () => { + await expect(validateAlias('_bibi')).rejects.toEqual( + new Error('Invalid characters in alias'), + ) + expect(logger.error).toBeCalledWith('Invalid characters in alias', '_bibi') + }) + }) + + describe('alias contains two following hyphens', () => { + it('throws and logs an error', async () => { + await expect(validateAlias('bi--bi')).rejects.toEqual( + new Error('Invalid characters in alias'), + ) + expect(logger.error).toBeCalledWith('Invalid characters in alias', 'bi--bi') + }) + }) + }) + + describe('test against existing alias in database', () => { + beforeAll(async () => { + const bibi = await userFactory(testEnv, bibiBloxberg) + const user = await User.findOne({ id: bibi.id }) + if (user) { + user.alias = 'b-b' + await user.save() + } + }) + + describe('alias exists in database', () => { + it('throws and logs an error', async () => { + await expect(validateAlias('b-b')).rejects.toEqual(new Error('Alias already in use')) + expect(logger.error).toBeCalledWith('Alias already in use', 'b-b') + }) + }) + + describe('alias exists in database with in lower-case', () => { + it('throws and logs an error', async () => { + await expect(validateAlias('b-B')).rejects.toEqual(new Error('Alias already in use')) + expect(logger.error).toBeCalledWith('Alias already in use', 'b-B') + }) + }) + + describe('valid alias', () => { + it('resolves to void', async () => { + await expect(validateAlias('bibi')).resolves.toEqual(undefined) + }) + }) + }) +}) diff --git a/backend/src/graphql/resolver/util/validateAlias.ts b/backend/src/graphql/resolver/util/validateAlias.ts new file mode 100644 index 000000000..d35b9e363 --- /dev/null +++ b/backend/src/graphql/resolver/util/validateAlias.ts @@ -0,0 +1,36 @@ +import { Raw } from '@dbTools/typeorm' +import { User as DbUser } from '@entity/User' + +import { LogError } from '@/server/LogError' + +const reservedAlias = [ + 'admin', + 'email', + 'gast', + 'gdd', + 'gradido', + 'guest', + 'home', + 'root', + 'support', + 'temp', + 'tmp', + 'tmp', + 'user', + 'usr', + 'var', +] + +export const validateAlias = async (alias: string): Promise => { + if (alias.length < 3) throw new LogError('Given alias is too short', alias) + if (alias.length > 20) throw new LogError('Given alias is too long', alias) + if (!alias.match(/^[0-9A-Za-z]([_-]?[A-Za-z0-9])+$/)) + throw new LogError('Invalid characters in alias', alias) + if (reservedAlias.includes(alias.toLowerCase())) throw new LogError('Alias is not allowed', alias) + const aliasInUse = await DbUser.find({ + where: { alias: Raw((a) => `LOWER(${a}) = "${alias.toLowerCase()}"`) }, + }) + if (aliasInUse.length !== 0) { + throw new LogError('Alias already in use', alias) + } +} From c78691c87c3e9ea2df0ee145993c70485b5948b1 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 10 May 2023 18:00:29 +0200 Subject: [PATCH 122/153] ignore unsage regex lint --- backend/src/graphql/resolver/util/validateAlias.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/graphql/resolver/util/validateAlias.ts b/backend/src/graphql/resolver/util/validateAlias.ts index d35b9e363..8528bf617 100644 --- a/backend/src/graphql/resolver/util/validateAlias.ts +++ b/backend/src/graphql/resolver/util/validateAlias.ts @@ -24,6 +24,7 @@ const reservedAlias = [ export const validateAlias = async (alias: string): Promise => { if (alias.length < 3) throw new LogError('Given alias is too short', alias) if (alias.length > 20) throw new LogError('Given alias is too long', alias) + /* eslint-disable-next-line security/detect-unsafe-regex */ if (!alias.match(/^[0-9A-Za-z]([_-]?[A-Za-z0-9])+$/)) throw new LogError('Invalid characters in alias', alias) if (reservedAlias.includes(alias.toLowerCase())) throw new LogError('Alias is not allowed', alias) From dbe752563be36565b39de1cc812fcaa9c4246a30 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 10 May 2023 18:39:41 +0200 Subject: [PATCH 123/153] Update backend/src/graphql/resolver/util/validateAlias.ts Co-authored-by: Ulf Gebhardt --- backend/src/graphql/resolver/util/validateAlias.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/util/validateAlias.ts b/backend/src/graphql/resolver/util/validateAlias.ts index 8528bf617..dcea7824c 100644 --- a/backend/src/graphql/resolver/util/validateAlias.ts +++ b/backend/src/graphql/resolver/util/validateAlias.ts @@ -21,7 +21,7 @@ const reservedAlias = [ 'var', ] -export const validateAlias = async (alias: string): Promise => { +export const validateAlias = async (alias: string): Promise => { if (alias.length < 3) throw new LogError('Given alias is too short', alias) if (alias.length > 20) throw new LogError('Given alias is too long', alias) /* eslint-disable-next-line security/detect-unsafe-regex */ @@ -34,4 +34,5 @@ export const validateAlias = async (alias: string): Promise => { if (aliasInUse.length !== 0) { throw new LogError('Alias already in use', alias) } + return true } From 1c924900199feeb4482e1a338aa1d6c4ceeecfd1 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 10 May 2023 18:39:52 +0200 Subject: [PATCH 124/153] Update backend/src/graphql/resolver/UserResolver.ts Co-authored-by: Ulf Gebhardt --- backend/src/graphql/resolver/UserResolver.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index bbd33f3ab..cc712e308 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -525,8 +525,7 @@ export class UserResolver { user.lastName = lastName } - if (alias) { - await validateAlias(alias) + if (alias && await validateAlias(alias)) { user.alias = alias } From d94543c1d9811e13fe84c315163d759dc6a9ead5 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 10 May 2023 20:47:28 +0200 Subject: [PATCH 125/153] fixes after merging suggestions --- backend/src/graphql/resolver/UserResolver.ts | 2 +- backend/src/graphql/resolver/util/validateAlias.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index cc712e308..0afbfcc5a 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -525,7 +525,7 @@ export class UserResolver { user.lastName = lastName } - if (alias && await validateAlias(alias)) { + if (alias && (await validateAlias(alias))) { user.alias = alias } diff --git a/backend/src/graphql/resolver/util/validateAlias.test.ts b/backend/src/graphql/resolver/util/validateAlias.test.ts index 733a09ffe..0cb790edb 100644 --- a/backend/src/graphql/resolver/util/validateAlias.test.ts +++ b/backend/src/graphql/resolver/util/validateAlias.test.ts @@ -117,8 +117,8 @@ describe('validate alias', () => { }) describe('valid alias', () => { - it('resolves to void', async () => { - await expect(validateAlias('bibi')).resolves.toEqual(undefined) + it('resolves to true', async () => { + await expect(validateAlias('bibi')).resolves.toEqual(true) }) }) }) From 8c62dd1a4f2883a44d640320e379d8e8a0c23389 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Thu, 11 May 2023 10:27:43 +0200 Subject: [PATCH 126/153] lint fixes --- backend/src/federation/client/Client.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/federation/client/Client.ts b/backend/src/federation/client/Client.ts index 515945eb5..98f63c127 100644 --- a/backend/src/federation/client/Client.ts +++ b/backend/src/federation/client/Client.ts @@ -10,12 +10,13 @@ import { Client_1_1 } from './Client_1_1' // eslint-disable-next-line camelcase type FederationClient = Client_1_0 | Client_1_1 -type ClientInstance = { +interface ClientInstance { 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[] = [] From 7203c2864df537b3d554f5ec7d4733aaac14775a Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 11 May 2023 13:29:49 +0200 Subject: [PATCH 127/153] fix(frontend): date fns locales --- admin/src/components/Fedaration/FederationVisualizeItem.vue | 2 +- frontend/src/components/TransactionRows/DurationRow.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/src/components/Fedaration/FederationVisualizeItem.vue b/admin/src/components/Fedaration/FederationVisualizeItem.vue index faace7da1..a947387f4 100644 --- a/admin/src/components/Fedaration/FederationVisualizeItem.vue +++ b/admin/src/components/Fedaration/FederationVisualizeItem.vue @@ -13,7 +13,7 @@ + diff --git a/frontend/src/graphql/mutations.js b/frontend/src/graphql/mutations.js index 802ea1818..8a281aad9 100644 --- a/frontend/src/graphql/mutations.js +++ b/frontend/src/graphql/mutations.js @@ -26,6 +26,7 @@ export const forgotPassword = gql` export const updateUserInfos = gql` mutation( + $alias: String $firstName: String $lastName: String $password: String @@ -35,6 +36,7 @@ export const updateUserInfos = gql` $hideAmountGDT: Boolean ) { updateUserInfos( + alias: $alias firstName: $firstName lastName: $lastName password: $password @@ -145,6 +147,7 @@ export const login = gql` mutation($email: String!, $password: String!, $publisherId: Int) { login(email: $email, password: $password, publisherId: $publisherId) { gradidoID + alias firstName lastName language diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index a5357e6d9..bfddf4405 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -166,6 +166,7 @@ "thx": "Danke", "to": "bis", "to1": "an", + "username": "Nutzername", "validation": { "gddCreationTime": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens einer Nachkommastelle sein", "gddSendAmount": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein", @@ -319,7 +320,11 @@ "subtitle": "Wenn du dein Passwort vergessen hast, kannst du es hier zurücksetzen." }, "showAmountGDD": "Dein GDD Betrag ist sichtbar.", - "showAmountGDT": "Dein GDT Betrag ist sichtbar." + "showAmountGDT": "Dein GDT Betrag ist sichtbar.", + "username": { + "change-username": "Nutzername ändern", + "change-success": "Dein Nutzername wurde erfolgreich geändert." + } }, "signin": "Anmelden", "signup": "Registrieren", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 42f22f709..b8c71a768 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -166,6 +166,7 @@ "thx": "Thank you", "to": "to", "to1": "to", + "username": "Username", "validation": { "gddCreationTime": "The field {_field_} must be a number between {min} and {max} with at most one decimal place.", "gddSendAmount": "The {_field_} field must be a number between {min} and {max} with at most two digits after the decimal point", @@ -319,7 +320,11 @@ "subtitle": "If you have forgotten your password, you can reset it here." }, "showAmountGDD": "Your GDD amount is visible.", - "showAmountGDT": "Your GDT amount is visible." + "showAmountGDT": "Your GDT amount is visible.", + "username": { + "change-username": "Change username", + "change-success": "Your username has been successfully changed." + } }, "signin": "Sign in", "signup": "Sign up", diff --git a/frontend/src/pages/Settings.vue b/frontend/src/pages/Settings.vue index 530484d9a..c5ca00f08 100644 --- a/frontend/src/pages/Settings.vue +++ b/frontend/src/pages/Settings.vue @@ -3,6 +3,8 @@
+ +

@@ -13,6 +15,7 @@ diff --git a/frontend/src/components/UserSettings/UserName.vue b/frontend/src/components/UserSettings/UserName.vue index 874fab61b..1066c6161 100644 --- a/frontend/src/components/UserSettings/UserName.vue +++ b/frontend/src/components/UserSettings/UserName.vue @@ -16,54 +16,59 @@
- - - - - {{ $t('form.username') }} - - - - - {{ username }} - -
- {{ $t('settings.username.no-username') }} -
-
- - - -
- - -
- - {{ $t('form.save') }} - -
-
-
-
+ + + + + + {{ $t('form.username') }} + + + + + {{ username }} + +
+ {{ $t('settings.username.no-username') }} +
+
+ + + +
+ + +
+ + {{ $t('form.save') }} + +
+
+
+
+
From 98f6a0c6d6918536c436dcd1041f0a9b8f428a8a Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 15 May 2023 21:40:25 +0200 Subject: [PATCH 136/153] localize username input --- frontend/src/components/UserSettings/UserName.vue | 6 +++++- frontend/src/locales/de.json | 1 + frontend/src/locales/en.json | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/UserSettings/UserName.vue b/frontend/src/components/UserSettings/UserName.vue index 1066c6161..9729be953 100644 --- a/frontend/src/components/UserSettings/UserName.vue +++ b/frontend/src/components/UserSettings/UserName.vue @@ -33,7 +33,11 @@ - + diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index c3082d9ee..c79357b4f 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -167,6 +167,7 @@ "to": "bis", "to1": "an", "username": "Nutzername", + "username-placeholder": "Gebe einen eindeutigen Nutzernamen ein", "validation": { "gddCreationTime": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens einer Nachkommastelle sein", "gddSendAmount": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index c9918d12e..170057b52 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -167,6 +167,7 @@ "to": "to", "to1": "to", "username": "Username", + "username-placeholder": "Enter a unique username", "validation": { "gddCreationTime": "The field {_field_} must be a number between {min} and {max} with at most one decimal place.", "gddSendAmount": "The {_field_} field must be a number between {min} and {max} with at most two digits after the decimal point", From c88e02e925ed60c685ea28968b04eb1a8833a1e5 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 15 May 2023 22:23:33 +0200 Subject: [PATCH 137/153] more username validation rules --- frontend/src/components/Inputs/InputUsername.vue | 4 +++- frontend/src/components/UserSettings/UserName.vue | 1 + frontend/src/locales/de.json | 3 ++- frontend/src/locales/en.json | 3 ++- frontend/src/validation-rules.js | 14 ++++++++++++++ 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Inputs/InputUsername.vue b/frontend/src/components/Inputs/InputUsername.vue index 8119ef6e0..ce07a3a51 100644 --- a/frontend/src/components/Inputs/InputUsername.vue +++ b/frontend/src/components/Inputs/InputUsername.vue @@ -35,7 +35,7 @@