From deb1d457e1a2f60815d713cfb5c7bf76cf843881 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 20 Apr 2022 10:32:33 +0200 Subject: [PATCH 01/68] migration adds is_admin column to users --- database/migrations/0034-drop_server_user_table.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 database/migrations/0034-drop_server_user_table.ts diff --git a/database/migrations/0034-drop_server_user_table.ts b/database/migrations/0034-drop_server_user_table.ts new file mode 100644 index 000000000..2e0a789ac --- /dev/null +++ b/database/migrations/0034-drop_server_user_table.ts @@ -0,0 +1,13 @@ +/* MIGRATION DROP server_users TABLE +add isAdmin COLUMN to users TABLE */ + +/* 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 `users` ADD COLUMN `is_admin` boolean DEFAULT false AFTER `language`;') +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('ALTER TABLE `users` DROP COLUMN `is_admin`;') +} From 58ab92ff63ba88071a02aaf807eff54044f096a1 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 20 Apr 2022 11:11:40 +0200 Subject: [PATCH 02/68] drop server users --- .../migrations/0034-drop_server_user_table.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/database/migrations/0034-drop_server_user_table.ts b/database/migrations/0034-drop_server_user_table.ts index 2e0a789ac..914c75457 100644 --- a/database/migrations/0034-drop_server_user_table.ts +++ b/database/migrations/0034-drop_server_user_table.ts @@ -6,8 +6,32 @@ add isAdmin COLUMN to users TABLE */ export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { await queryFn('ALTER TABLE `users` ADD COLUMN `is_admin` boolean DEFAULT false AFTER `language`;') + + await queryFn( + 'UPDATE `users` SET `is_admin` = true WHERE `email` IN (SELECT `email` FROM `server_users`);', + ) + + await queryFn('DROP TABLE `server_users`;') } export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn(` + CREATE TABLE IF NOT EXISTS \`server_users\` ( + \`id\` int(10) unsigned NOT NULL AUTO_INCREMENT, + \`username\` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + \`password\` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + \`email\` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + \`role\` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'admin', + \`activated\` tinyint(4) NOT NULL DEFAULT '0', + \`last_login\` datetime DEFAULT NULL, + \`created\` datetime NOT NULL, + \`modified\` datetime NOT NULL, + PRIMARY KEY (\`id\`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`) + + await queryFn( + 'INSERT INTO `server_users` (`email`, `username`, `password`, `created`, `modified`) SELECT `email`, `first_name`, `password`, `created`, `created` FROM `users` WHERE `is_admin` = true;', + ) + await queryFn('ALTER TABLE `users` DROP COLUMN `is_admin`;') } From ca4a92d321d804dda4c8c4a79b0bea716616ff1b Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 20 Apr 2022 11:19:02 +0200 Subject: [PATCH 03/68] add isAdmin entity to user db model --- .../0034-drop_server_user_table/User.ts | 81 +++++++++++++++++++ database/entity/User.ts | 2 +- 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 database/entity/0034-drop_server_user_table/User.ts diff --git a/database/entity/0034-drop_server_user_table/User.ts b/database/entity/0034-drop_server_user_table/User.ts new file mode 100644 index 000000000..9192b92ff --- /dev/null +++ b/database/entity/0034-drop_server_user_table/User.ts @@ -0,0 +1,81 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + OneToMany, + DeleteDateColumn, +} from 'typeorm' +import { UserSetting } from '../UserSetting' + +@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) +export class User extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true }) + pubKey: Buffer + + @Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true }) + privKey: Buffer + + @Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' }) + email: string + + @Column({ + name: 'first_name', + length: 255, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + firstName: string + + @Column({ + name: 'last_name', + length: 255, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + lastName: string + + @DeleteDateColumn() + deletedAt: Date | null + + @Column({ type: 'bigint', default: 0, unsigned: true }) + password: BigInt + + @Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true }) + emailHash: Buffer + + @Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false }) + createdAt: Date + + @Column({ name: 'email_checked', type: 'bool', nullable: false, default: false }) + emailChecked: boolean + + @Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false }) + language: string + + @Column({ name: 'is_admin', type: 'bool', nullable: false, default: false }) + isAdmin: boolean + + @Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null }) + referrerId?: number | null + + @Column({ name: 'publisher_id', default: 0 }) + publisherId: number + + @Column({ + type: 'text', + name: 'passphrase', + collation: 'utf8mb4_unicode_ci', + nullable: true, + default: null, + }) + passphrase: string + + @OneToMany(() => UserSetting, (userSetting) => userSetting.user) + settings: UserSetting[] +} diff --git a/database/entity/User.ts b/database/entity/User.ts index 35dfb7bbe..4cd68174c 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0033-add_referrer_id/User' +export { User } from './0034-drop_server_user_table/User' From 5c6aa5d4ce715911dd60128ac6448b4c807e9c6e Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 20 Apr 2022 11:20:28 +0200 Subject: [PATCH 04/68] new db version for isAdmin in user db model --- backend/src/config/index.ts | 2 +- backend/src/util/communityUser.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 91f450369..4bf550038 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0033-add_referrer_id', + DB_VERSION: '0034-drop_server_user_table', DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0 CONFIG_VERSION: { DEFAULT: 'DEFAULT', diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index 33ac2fad2..8b75b37a1 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -17,6 +17,7 @@ const communityDbUser: dbUser = { createdAt: new Date(), emailChecked: false, language: '', + isAdmin: false, publisherId: 0, passphrase: '', settings: [], From 64859a71f44457afc97b31f2fbe225451dedb90b Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 20 Apr 2022 11:27:09 +0200 Subject: [PATCH 05/68] use isAdmin on user to determine if user is admin --- backend/src/graphql/model/User.ts | 8 ++++---- backend/src/graphql/resolver/UserResolver.ts | 10 ---------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index 1a187a38f..1949592c0 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -14,8 +14,8 @@ export class User { this.emailChecked = user.emailChecked this.language = user.language this.publisherId = user.publisherId + this.isAdmin = user.isAdmin // TODO - this.isAdmin = null this.coinanimation = null this.klickTipp = null this.hasElopage = null @@ -58,11 +58,11 @@ export class User { // `passphrase` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, + @Field(() => Boolean) + isAdmin: boolean + // TODO this is a bit inconsistent with what we query from the database // therefore all those fields are now nullable with default value null - @Field(() => Boolean, { nullable: true }) - isAdmin: boolean | null - @Field(() => Boolean, { nullable: true }) coinanimation: boolean | null diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 137c09622..7685268b4 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -19,9 +19,7 @@ import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' import { klicktippSignIn } from '@/apis/KlicktippController' import { RIGHTS } from '@/auth/RIGHTS' -import { ROLE_ADMIN } from '@/auth/ROLES' import { hasElopageBuys } from '@/util/hasElopageBuys' -import { ServerUser } from '@entity/ServerUser' // eslint-disable-next-line @typescript-eslint/no-var-requires const sodium = require('sodium-native') @@ -207,7 +205,6 @@ export class UserResolver { }) user.coinanimation = coinanimation - user.isAdmin = context.role === ROLE_ADMIN return user } @@ -243,9 +240,6 @@ export class UserResolver { } const user = new User(dbUser) - // user.email = email - // user.pubkey = dbUser.pubKey.toString('hex') - user.language = dbUser.language // Elopage Status & Stored PublisherId user.hasElopage = await this.hasElopage({ ...context, user: dbUser }) @@ -266,10 +260,6 @@ export class UserResolver { }) user.coinanimation = coinanimation - // context.role is not set to the actual role yet on login - const countServerUsers = await ServerUser.count({ email: user.email }) - user.isAdmin = countServerUsers > 0 - context.setHeaders.push({ key: 'token', value: encode(dbUser.pubKey), From 8ca72beac8cdbd5ebf112f90d09652e398ad41c5 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 20 Apr 2022 11:30:29 +0200 Subject: [PATCH 06/68] use isAdmin of user to determine user role --- backend/src/graphql/directive/isAuthorized.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/src/graphql/directive/isAuthorized.ts b/backend/src/graphql/directive/isAuthorized.ts index 84756c45a..065c01957 100644 --- a/backend/src/graphql/directive/isAuthorized.ts +++ b/backend/src/graphql/directive/isAuthorized.ts @@ -8,7 +8,6 @@ import { RIGHTS } from '@/auth/RIGHTS' import { getCustomRepository } from '@dbTools/typeorm' import { UserRepository } from '@repository/User' import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS' -import { ServerUser } from '@entity/ServerUser' const isAuthorized: AuthChecker = async ({ context }, rights) => { context.role = ROLE_UNAUTHORIZED // unauthorized user @@ -36,8 +35,7 @@ const isAuthorized: AuthChecker = async ({ context }, rights) => { try { const user = await userRepository.findByPubkeyHex(context.pubKey) context.user = user - const countServerUsers = await ServerUser.count({ email: user.email }) - context.role = countServerUsers > 0 ? ROLE_ADMIN : ROLE_USER + context.role = user.isAdmin ? ROLE_ADMIN : ROLE_USER } catch { // in case the database query fails (user deleted) throw new Error('401 Unauthorized') From 9136c4596dad00460523b789718ede5871fa80ab Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 20 Apr 2022 11:32:30 +0200 Subject: [PATCH 07/68] remove server user entity --- database/entity/ServerUser.ts | 1 - database/entity/index.ts | 2 -- 2 files changed, 3 deletions(-) delete mode 100644 database/entity/ServerUser.ts diff --git a/database/entity/ServerUser.ts b/database/entity/ServerUser.ts deleted file mode 100644 index 495513823..000000000 --- a/database/entity/ServerUser.ts +++ /dev/null @@ -1 +0,0 @@ -export { ServerUser } from './0001-init_db/ServerUser' diff --git a/database/entity/index.ts b/database/entity/index.ts index cb6f56ab0..542333755 100644 --- a/database/entity/index.ts +++ b/database/entity/index.ts @@ -1,7 +1,6 @@ import { LoginElopageBuys } from './LoginElopageBuys' import { LoginEmailOptIn } from './LoginEmailOptIn' import { Migration } from './Migration' -import { ServerUser } from './ServerUser' import { Transaction } from './Transaction' import { TransactionLink } from './TransactionLink' import { User } from './User' @@ -13,7 +12,6 @@ export const entities = [ LoginElopageBuys, LoginEmailOptIn, Migration, - ServerUser, Transaction, TransactionLink, User, From c9701574f38cf28d3ad26a3a37b02dd0bafa82df Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 20 Apr 2022 11:44:34 +0200 Subject: [PATCH 08/68] seed with isAdmin, test user resolver with isAdmin --- .../src/graphql/resolver/UserResolver.test.ts | 1 + backend/src/seeds/factory/user.ts | 23 ++++--------------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 07b8e59e2..b9e230afe 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -100,6 +100,7 @@ describe('UserResolver', () => { emailChecked: false, passphrase: expect.any(String), language: 'de', + isAdmin: false, deletedAt: null, publisherId: 1234, referrerId: null, diff --git a/backend/src/seeds/factory/user.ts b/backend/src/seeds/factory/user.ts index ff4c1d6c9..373a3da4f 100644 --- a/backend/src/seeds/factory/user.ts +++ b/backend/src/seeds/factory/user.ts @@ -1,7 +1,6 @@ import { createUser, setPassword } from '@/seeds/graphql/mutations' import { User } from '@entity/User' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' -import { ServerUser } from '@entity/ServerUser' import { UserInterface } from '@/seeds/users/UserInterface' import { ApolloServerTestClient } from 'apollo-server-testing' @@ -29,23 +28,9 @@ export const userFactory = async ( // get user from database const dbUser = await User.findOneOrFail({ id }) - if (user.createdAt || user.deletedAt) { - if (user.createdAt) dbUser.createdAt = user.createdAt - if (user.deletedAt) dbUser.deletedAt = user.deletedAt - await dbUser.save() - } - - if (user.isAdmin) { - const admin = new ServerUser() - admin.username = dbUser.firstName - admin.password = 'please_refactor' - admin.email = dbUser.email - admin.role = 'admin' - admin.activated = 1 - admin.lastLogin = new Date() - admin.created = dbUser.createdAt - admin.modified = dbUser.createdAt - await admin.save() - } + if (user.createdAt) dbUser.createdAt = user.createdAt + if (user.deletedAt) dbUser.deletedAt = user.deletedAt + if (user.isAdmin) dbUser.isAdmin = user.isAdmin + await dbUser.save() } } From 863ecfa4746c4f4f3a0bf5127119e0cce7251520 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 21 Apr 2022 10:58:22 +0200 Subject: [PATCH 09/68] migrate is_admin column in users as datetime with default null --- database/migrations/0034-drop_server_user_table.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/database/migrations/0034-drop_server_user_table.ts b/database/migrations/0034-drop_server_user_table.ts index 914c75457..fd5a9a682 100644 --- a/database/migrations/0034-drop_server_user_table.ts +++ b/database/migrations/0034-drop_server_user_table.ts @@ -5,10 +5,10 @@ add isAdmin COLUMN to users TABLE */ /* eslint-disable @typescript-eslint/no-explicit-any */ export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { - await queryFn('ALTER TABLE `users` ADD COLUMN `is_admin` boolean DEFAULT false AFTER `language`;') + await queryFn('ALTER TABLE `users` ADD COLUMN `is_admin` datetime DEFAULT NULL AFTER `language`;') await queryFn( - 'UPDATE `users` SET `is_admin` = true WHERE `email` IN (SELECT `email` FROM `server_users`);', + 'UPDATE `users` AS `users`, (SELECT * FROM `server_users`) AS `server_users` SET users.`is_admin` = server_users.`modified` WHERE users.`email` IN (SELECT email from `server_users`);', ) await queryFn('DROP TABLE `server_users`;') @@ -30,7 +30,7 @@ export async function downgrade(queryFn: (query: string, values?: any[]) => Prom ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`) await queryFn( - 'INSERT INTO `server_users` (`email`, `username`, `password`, `created`, `modified`) SELECT `email`, `first_name`, `password`, `created`, `created` FROM `users` WHERE `is_admin` = true;', + 'INSERT INTO `server_users` (`email`, `username`, `password`, `created`, `modified`) SELECT `email`, `first_name`, `password`, `is_admin`, `is_admin` FROM `users` WHERE `is_admin` IS NOT NULL;', ) await queryFn('ALTER TABLE `users` DROP COLUMN `is_admin`;') From ba648c67a98dd31c1bf07f494ae47529399a7364 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 21 Apr 2022 11:01:23 +0200 Subject: [PATCH 10/68] entity model users.isAdmin as nullable date --- database/entity/0034-drop_server_user_table/User.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/database/entity/0034-drop_server_user_table/User.ts b/database/entity/0034-drop_server_user_table/User.ts index 9192b92ff..1f56d13d2 100644 --- a/database/entity/0034-drop_server_user_table/User.ts +++ b/database/entity/0034-drop_server_user_table/User.ts @@ -58,8 +58,8 @@ export class User extends BaseEntity { @Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false }) language: string - @Column({ name: 'is_admin', type: 'bool', nullable: false, default: false }) - isAdmin: boolean + @Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null }) + isAdmin: Date | null @Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null }) referrerId?: number | null From fa6fbe38c8e0ba1b44592146602cfb3938d9242d Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 21 Apr 2022 11:05:51 +0200 Subject: [PATCH 11/68] User.isAdmin as nullable Date --- backend/src/graphql/model/User.ts | 4 ++-- backend/src/seeds/factory/user.ts | 2 +- backend/src/util/communityUser.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index 1949592c0..4f577f60a 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -58,8 +58,8 @@ export class User { // `passphrase` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, - @Field(() => Boolean) - isAdmin: boolean + @Field(() => Date, { nullable: true }) + isAdmin: Date | null // TODO this is a bit inconsistent with what we query from the database // therefore all those fields are now nullable with default value null diff --git a/backend/src/seeds/factory/user.ts b/backend/src/seeds/factory/user.ts index 373a3da4f..4b5913d48 100644 --- a/backend/src/seeds/factory/user.ts +++ b/backend/src/seeds/factory/user.ts @@ -30,7 +30,7 @@ export const userFactory = async ( if (user.createdAt) dbUser.createdAt = user.createdAt if (user.deletedAt) dbUser.deletedAt = user.deletedAt - if (user.isAdmin) dbUser.isAdmin = user.isAdmin + if (user.isAdmin) dbUser.isAdmin = new Date() await dbUser.save() } } diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index 8b75b37a1..0d0d12f6c 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -17,7 +17,7 @@ const communityDbUser: dbUser = { createdAt: new Date(), emailChecked: false, language: '', - isAdmin: false, + isAdmin: null, publisherId: 0, passphrase: '', settings: [], From 50bddf49e4a1de785e5e413999c6c69186de2d84 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 21 Apr 2022 11:16:14 +0200 Subject: [PATCH 12/68] fix test for User.isAdmin as nullable date --- backend/src/graphql/resolver/UserResolver.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index b9e230afe..8e5cb299d 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -100,7 +100,7 @@ describe('UserResolver', () => { emailChecked: false, passphrase: expect.any(String), language: 'de', - isAdmin: false, + isAdmin: null, deletedAt: null, publisherId: 1234, referrerId: null, @@ -337,7 +337,7 @@ describe('UserResolver', () => { firstName: 'Bibi', hasElopage: false, id: expect.any(Number), - isAdmin: false, + isAdmin: null, klickTipp: { newsletterState: false, }, From 39775868ee482f50e074d05e885f92119b77ce3c Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Thu, 21 Apr 2022 13:41:55 +0200 Subject: [PATCH 13/68] federation image created for federation planning --- docu/graphics/federation.drawio | 135 ++++++++++++++++++++++++++++++++ docu/graphics/federation.png | Bin 0 -> 104264 bytes 2 files changed, 135 insertions(+) create mode 100644 docu/graphics/federation.drawio create mode 100644 docu/graphics/federation.png diff --git a/docu/graphics/federation.drawio b/docu/graphics/federation.drawio new file mode 100644 index 000000000..1b4db9002 --- /dev/null +++ b/docu/graphics/federation.drawio @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docu/graphics/federation.png b/docu/graphics/federation.png new file mode 100644 index 0000000000000000000000000000000000000000..6d25708f8c59454b13bae33bc4af4a7a212c2cd3 GIT binary patch literal 104264 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 Date: Wed, 6 Apr 2022 14:46:45 +0200 Subject: [PATCH 14/68] convert admin_pending_creations to decimal --- .../AdminPendingCreation.ts | 33 +++++++++++++++++++ database/entity/AdminPendingCreation.ts | 2 +- .../0034-admin_pending_creations_decimal.ts | 33 +++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 database/entity/0034-admin_pending_creations_decimal/AdminPendingCreation.ts create mode 100644 database/migrations/0034-admin_pending_creations_decimal.ts diff --git a/database/entity/0034-admin_pending_creations_decimal/AdminPendingCreation.ts b/database/entity/0034-admin_pending_creations_decimal/AdminPendingCreation.ts new file mode 100644 index 000000000..d204942b3 --- /dev/null +++ b/database/entity/0034-admin_pending_creations_decimal/AdminPendingCreation.ts @@ -0,0 +1,33 @@ +import Decimal from 'decimal.js-light' +import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm' +import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' + +@Entity('admin_pending_creations') +export class AdminPendingCreation extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ unsigned: true, nullable: false }) + userId: number + + @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) + created: Date + + @Column({ type: 'datetime', nullable: false }) + date: Date + + @Column({ length: 256, nullable: true, default: null }) + memo: string + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: false, + transformer: DecimalTransformer, + }) + amount: Decimal + + @Column() + moderator: number +} diff --git a/database/entity/AdminPendingCreation.ts b/database/entity/AdminPendingCreation.ts index 03eeab883..a32590a22 100644 --- a/database/entity/AdminPendingCreation.ts +++ b/database/entity/AdminPendingCreation.ts @@ -1 +1 @@ -export { AdminPendingCreation } from './0015-admin_pending_creations/AdminPendingCreation' +export { AdminPendingCreation } from './0034-admin_pending_creations_decimal/AdminPendingCreation' diff --git a/database/migrations/0034-admin_pending_creations_decimal.ts b/database/migrations/0034-admin_pending_creations_decimal.ts new file mode 100644 index 000000000..6df1e563b --- /dev/null +++ b/database/migrations/0034-admin_pending_creations_decimal.ts @@ -0,0 +1,33 @@ +/* MIGRATION TO CHANGE `amount` FIELD TYPE TO `Decimal` ON `admin_pending_creations` */ + +/* 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>) { + // rename `amount` to `amount_bigint` + await queryFn('ALTER TABLE `admin_pending_creations` RENAME COLUMN `amount` TO `amount_bigint`;') + // add `amount` (decimal) + await queryFn( + 'ALTER TABLE `admin_pending_creations` ADD COLUMN `amount` DECIMAL(40,20) DEFAULT NULL AFTER `amount_bigint`;', + ) + // fill new `amount` column + await queryFn('UPDATE `admin_pending_creations` SET `amount` = `amount_bigint` DIV 10000;') + // make `amount` not nullable + await queryFn( + 'ALTER TABLE `admin_pending_creations` MODIFY COLUMN `amount` DECIMAL(40,20) NOT NULL;', + ) + // drop `amount_bitint` column + await queryFn('ALTER TABLE `admin_pending_creations` DROP COLUMN `amount_bigint`;') +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn( + 'ALTER TABLE `admin_pending_creations` ADD COLUMN `amount_bigint` bigint(20) DEFAULT NULL AFTER `amount`;', + ) + await queryFn('UPDATE `admin_pending_creations` SET `amount_bigint` = `amount` * 10000;') + await queryFn( + 'ALTER TABLE `admin_pending_creations` MODIFY COLUMN `amount_bigint` bigint(20) NOT NULL;', + ) + await queryFn('ALTER TABLE `admin_pending_creations` DROP COLUMN `amount`;') + await queryFn('ALTER TABLE `admin_pending_creations` RENAME COLUMN `amount_bigint` TO `amount`;') +} From 56ed35d6928a3a38c97b33c27c888fa346aafad7 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 6 Apr 2022 14:48:28 +0200 Subject: [PATCH 15/68] backend fix types for admin_pending_creations is now decimal --- .../graphql/arg/CreatePendingCreationArgs.ts | 7 +++-- .../graphql/arg/UpdatePendingCreationArgs.ts | 7 +++-- backend/src/graphql/model/PendingCreation.ts | 9 +++--- .../graphql/model/UpdatePendingCreation.ts | 9 +++--- backend/src/graphql/model/UserAdmin.ts | 9 +++--- backend/src/graphql/resolver/AdminResolver.ts | 28 +++++++++---------- 6 files changed, 37 insertions(+), 32 deletions(-) diff --git a/backend/src/graphql/arg/CreatePendingCreationArgs.ts b/backend/src/graphql/arg/CreatePendingCreationArgs.ts index b90ad3231..0cadf5e62 100644 --- a/backend/src/graphql/arg/CreatePendingCreationArgs.ts +++ b/backend/src/graphql/arg/CreatePendingCreationArgs.ts @@ -1,4 +1,5 @@ -import { ArgsType, Field, Float, InputType, Int } from 'type-graphql' +import { ArgsType, Field, InputType, Int } from 'type-graphql' +import Decimal from 'decimal.js-light' @InputType() @ArgsType() @@ -6,8 +7,8 @@ export default class CreatePendingCreationArgs { @Field(() => String) email: string - @Field(() => Float) - amount: number + @Field(() => Decimal) + amount: Decimal @Field(() => String) memo: string diff --git a/backend/src/graphql/arg/UpdatePendingCreationArgs.ts b/backend/src/graphql/arg/UpdatePendingCreationArgs.ts index 73f70c058..3cd85e84b 100644 --- a/backend/src/graphql/arg/UpdatePendingCreationArgs.ts +++ b/backend/src/graphql/arg/UpdatePendingCreationArgs.ts @@ -1,4 +1,5 @@ -import { ArgsType, Field, Float, Int } from 'type-graphql' +import { ArgsType, Field, Int } from 'type-graphql' +import Decimal from 'decimal.js-light' @ArgsType() export default class UpdatePendingCreationArgs { @@ -8,8 +9,8 @@ export default class UpdatePendingCreationArgs { @Field(() => String) email: string - @Field(() => Float) - amount: number + @Field(() => Decimal) + amount: Decimal @Field(() => String) memo: string diff --git a/backend/src/graphql/model/PendingCreation.ts b/backend/src/graphql/model/PendingCreation.ts index 594657a59..500ba6f6b 100644 --- a/backend/src/graphql/model/PendingCreation.ts +++ b/backend/src/graphql/model/PendingCreation.ts @@ -1,4 +1,5 @@ import { ObjectType, Field, Int } from 'type-graphql' +import Decimal from 'decimal.js-light' @ObjectType() export class PendingCreation { @@ -23,12 +24,12 @@ export class PendingCreation { @Field(() => String) memo: string - @Field(() => Number) - amount: number + @Field(() => Decimal) + amount: Decimal @Field(() => Number) moderator: number - @Field(() => [Number]) - creation: number[] + @Field(() => [Decimal]) + creation: Decimal[] } diff --git a/backend/src/graphql/model/UpdatePendingCreation.ts b/backend/src/graphql/model/UpdatePendingCreation.ts index c8033f86e..85d3af2cc 100644 --- a/backend/src/graphql/model/UpdatePendingCreation.ts +++ b/backend/src/graphql/model/UpdatePendingCreation.ts @@ -1,4 +1,5 @@ import { ObjectType, Field } from 'type-graphql' +import Decimal from 'decimal.js-light' @ObjectType() export class UpdatePendingCreation { @@ -8,12 +9,12 @@ export class UpdatePendingCreation { @Field(() => String) memo: string - @Field(() => Number) - amount: number + @Field(() => Decimal) + amount: Decimal @Field(() => Number) moderator: number - @Field(() => [Number]) - creation: number[] + @Field(() => [Decimal]) + creation: Decimal[] } diff --git a/backend/src/graphql/model/UserAdmin.ts b/backend/src/graphql/model/UserAdmin.ts index 1d418c66c..8a1459c0f 100644 --- a/backend/src/graphql/model/UserAdmin.ts +++ b/backend/src/graphql/model/UserAdmin.ts @@ -1,9 +1,10 @@ -import { User } from '@entity/User' import { ObjectType, Field, Int } from 'type-graphql' +import Decimal from 'decimal.js-light' +import { User } from '@entity/User' @ObjectType() export class UserAdmin { - constructor(user: User, creation: number[], hasElopage: boolean, emailConfirmationSend: string) { + constructor(user: User, creation: Decimal[], hasElopage: boolean, emailConfirmationSend: string) { this.userId = user.id this.email = user.email this.firstName = user.firstName @@ -27,8 +28,8 @@ export class UserAdmin { @Field(() => String) lastName: string - @Field(() => [Number]) - creation: number[] + @Field(() => [Decimal]) + creation: Decimal[] @Field(() => Boolean) emailChecked: boolean diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 7ca3460ee..379412cdc 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -43,7 +43,7 @@ import CONFIG from '@/config' // const EMAIL_OPT_IN_REGISTER = 1 // const EMAIL_OPT_UNKNOWN = 3 // elopage? -const MAX_CREATION_AMOUNT = 1000 +const MAX_CREATION_AMOUNT = new Decimal(1000) const FULL_CREATION_AVAILABLE = [MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT] @Resolver() @@ -170,7 +170,7 @@ export class AdminResolver { @Mutation(() => [Number]) async createPendingCreation( @Args() { email, amount, memo, creationDate, moderator }: CreatePendingCreationArgs, - ): Promise { + ): Promise { const user = await dbUser.findOne({ email }, { withDeleted: true }) if (!user) { throw new Error(`Could not find user with email: ${email}`) @@ -186,7 +186,7 @@ export class AdminResolver { if (isCreationValid(creations, amount, creationDateObj)) { const adminPendingCreation = AdminPendingCreation.create() adminPendingCreation.userId = user.id - adminPendingCreation.amount = BigInt(amount) + adminPendingCreation.amount = amount adminPendingCreation.created = new Date() adminPendingCreation.date = creationDateObj adminPendingCreation.memo = memo @@ -251,14 +251,14 @@ export class AdminResolver { if (!isCreationValid(creations, amount, creationDateObj)) { throw new Error('Creation is not valid') } - pendingCreationToUpdate.amount = BigInt(amount) + pendingCreationToUpdate.amount = amount pendingCreationToUpdate.memo = memo pendingCreationToUpdate.date = new Date(creationDate) pendingCreationToUpdate.moderator = moderator await AdminPendingCreation.save(pendingCreationToUpdate) const result = new UpdatePendingCreation() - result.amount = parseInt(amount.toString()) + result.amount = amount result.memo = pendingCreationToUpdate.memo result.date = pendingCreationToUpdate.date result.moderator = pendingCreationToUpdate.moderator @@ -286,7 +286,7 @@ export class AdminResolver { return { ...pendingCreation, - amount: Number(pendingCreation.amount.toString()), + amount: pendingCreation.amount, firstName: user ? user.firstName : '', lastName: user ? user.lastName : '', email: user ? user.email : '', @@ -318,7 +318,7 @@ export class AdminResolver { if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a creation.') const creations = await getUserCreation(pendingCreation.userId, false) - if (!isCreationValid(creations, Number(pendingCreation.amount), pendingCreation.date)) { + if (!isCreationValid(creations, pendingCreation.amount, pendingCreation.date)) { throw new Error('Creation is not valid!!') } @@ -448,10 +448,10 @@ export class AdminResolver { interface CreationMap { id: number - creations: number[] + creations: Decimal[] } -async function getUserCreation(id: number, includePending = true): Promise { +async function getUserCreation(id: number, includePending = true): Promise { const creations = await getUserCreations([id], includePending) return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE } @@ -493,30 +493,30 @@ async function getUserCreations(ids: number[], includePending = true): Promise parseInt(raw.month) === month && parseInt(raw.id) === id, ) - return MAX_CREATION_AMOUNT - (creation ? Number(creation.sum) : 0) + return MAX_CREATION_AMOUNT.minus(creation ? creation.sum : 0) }), } }) } -function updateCreations(creations: number[], pendingCreation: AdminPendingCreation): number[] { +function updateCreations(creations: Decimal[], pendingCreation: AdminPendingCreation): Decimal[] { const index = getCreationIndex(pendingCreation.date.getMonth()) if (index < 0) { throw new Error('You cannot create GDD for a month older than the last three months.') } - creations[index] += parseInt(pendingCreation.amount.toString()) + creations[index] = creations[index].plus(pendingCreation.amount) return creations } -function isCreationValid(creations: number[], amount: number, creationDate: Date) { +function isCreationValid(creations: Decimal[], amount: Decimal, creationDate: Date) { const index = getCreationIndex(creationDate.getMonth()) if (index < 0) { throw new Error(`No Creation found!`) } - if (amount > creations[index]) { + if (amount.greaterThan(creations[index])) { throw new Error( `The amount (${amount} GDD) to be created exceeds the available amount (${creations[index]} GDD) for this month.`, ) From 7b86c4bc2470fa589977a25f2de2949e6cd9ed8e Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 6 Apr 2022 14:53:03 +0200 Subject: [PATCH 16/68] admin use decimal instead of float for pending creations in graphql queries --- admin/src/graphql/createPendingCreation.js | 2 +- admin/src/graphql/updatePendingCreation.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/src/graphql/createPendingCreation.js b/admin/src/graphql/createPendingCreation.js index 183fa5b15..05402ed9f 100644 --- a/admin/src/graphql/createPendingCreation.js +++ b/admin/src/graphql/createPendingCreation.js @@ -3,7 +3,7 @@ import gql from 'graphql-tag' export const createPendingCreation = gql` mutation ( $email: String! - $amount: Float! + $amount: Decimal! $memo: String! $creationDate: String! $moderator: Int! diff --git a/admin/src/graphql/updatePendingCreation.js b/admin/src/graphql/updatePendingCreation.js index 77668f15b..cd0ae6c8e 100644 --- a/admin/src/graphql/updatePendingCreation.js +++ b/admin/src/graphql/updatePendingCreation.js @@ -4,7 +4,7 @@ export const updatePendingCreation = gql` mutation ( $id: Int! $email: String! - $amount: Float! + $amount: Decimal! $memo: String! $creationDate: String! $moderator: Int! From efcd75717a098abb670ee469073992192ac18dd6 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 20 Apr 2022 14:24:41 +0200 Subject: [PATCH 17/68] updated backend required database version --- backend/src/config/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 91f450369..1f134f23d 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0033-add_referrer_id', + DB_VERSION: '0034-admin_pending_creations_decimal.ts', DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0 CONFIG_VERSION: { DEFAULT: 'DEFAULT', From eb2b0e951eee0d56c2bb3603e309abdefaec1fdd Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 21 Apr 2022 14:04:04 +0200 Subject: [PATCH 18/68] reset transaction link arrays after click --- .../Transactions/TransactionLinkSummary.vue | 163 +++++++++--------- 1 file changed, 82 insertions(+), 81 deletions(-) diff --git a/frontend/src/components/Transactions/TransactionLinkSummary.vue b/frontend/src/components/Transactions/TransactionLinkSummary.vue index 84d0f9b84..b1ba40c69 100644 --- a/frontend/src/components/Transactions/TransactionLinkSummary.vue +++ b/frontend/src/components/Transactions/TransactionLinkSummary.vue @@ -37,86 +37,87 @@ From 839d52748b1737ed5d8e6556760f09856a521929 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 21 Apr 2022 14:08:22 +0200 Subject: [PATCH 19/68] test reopening of transaction link details --- .../TransactionLinkSummary.spec.js | 21 +++ .../Transactions/TransactionLinkSummary.vue | 164 +++++++++--------- 2 files changed, 103 insertions(+), 82 deletions(-) diff --git a/frontend/src/components/Transactions/TransactionLinkSummary.spec.js b/frontend/src/components/Transactions/TransactionLinkSummary.spec.js index 3e57bceeb..79b453344 100644 --- a/frontend/src/components/Transactions/TransactionLinkSummary.spec.js +++ b/frontend/src/components/Transactions/TransactionLinkSummary.spec.js @@ -132,6 +132,27 @@ describe('TransactionLinkSummary', () => { it('has no component CollapseLinksList', () => { expect(wrapper.findComponent({ name: 'CollapseLinksList' }).isVisible()).toBe(false) }) + + describe('reopen transaction link details', () => { + beforeEach(() => { + jest.clearAllMocks() + wrapper.find('div.transaction-link-details').trigger('click') + }) + + it('calls the API to get the list transaction links', () => { + expect(apolloQueryMock).toBeCalledWith({ + query: listTransactionLinks, + variables: { + currentPage: 1, + }, + fetchPolicy: 'network-only', + }) + }) + + it('has four transactionLinks', () => { + expect(wrapper.vm.transactionLinks).toHaveLength(4) + }) + }) }) describe('load more transaction links', () => { diff --git a/frontend/src/components/Transactions/TransactionLinkSummary.vue b/frontend/src/components/Transactions/TransactionLinkSummary.vue index b1ba40c69..c24410924 100644 --- a/frontend/src/components/Transactions/TransactionLinkSummary.vue +++ b/frontend/src/components/Transactions/TransactionLinkSummary.vue @@ -37,87 +37,87 @@ From bbec1306d041e7661022660e27ada0f1746ec46d Mon Sep 17 00:00:00 2001 From: ogerly Date: Thu, 21 Apr 2022 16:38:14 +0200 Subject: [PATCH 20/68] change modal structure --- .../TransactionLinks/TransactionLink.vue | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/TransactionLinks/TransactionLink.vue b/frontend/src/components/TransactionLinks/TransactionLink.vue index 66f9f2f92..40d9296f1 100644 --- a/frontend/src/components/TransactionLinks/TransactionLink.vue +++ b/frontend/src/components/TransactionLinks/TransactionLink.vue @@ -45,11 +45,16 @@ - -
- -

{{ link }}

-
+ + + + + + From e154373694b3534c35b3b193a952ef0df2a60cb8 Mon Sep 17 00:00:00 2001 From: ogerly Date: Thu, 21 Apr 2022 16:39:19 +0200 Subject: [PATCH 21/68] add locales 'qrCode' --- frontend/src/locales/de.json | 1 + frontend/src/locales/en.json | 1 + 2 files changed, 2 insertions(+) diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 20ce055d4..d7a6be144 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -164,6 +164,7 @@ "infoText": "Wenn dir dein Empfehlungsgeber seine Publisher-Id gegeben hat, trage sie hier ein, sonst lass das Feld bitte unverändert!", "publisherId": "Publisher-Id:" }, + "qrCode":"QR Code", "send_gdd": "GDD versenden", "send_per_link": "GDD versenden per Link", "settings": { diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 201c44d93..a72a4ef25 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -164,6 +164,7 @@ "infoText": "If your referrer has given you his publisher id, enter it here, otherwise leave the field unchanged!", "publisherId": "PublisherID:" }, + "qrCode":"QR Code", "send_gdd": "GDD send", "send_per_link": "GDD send via link", "settings": { From 0e8ca5b410c016c76d7f11a16e2e8a29a88d723b Mon Sep 17 00:00:00 2001 From: ogerly Date: Thu, 21 Apr 2022 16:40:30 +0200 Subject: [PATCH 22/68] fixed yarn lint --- frontend/src/locales/de.json | 2 +- frontend/src/locales/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index d7a6be144..6c891d4b2 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -164,7 +164,7 @@ "infoText": "Wenn dir dein Empfehlungsgeber seine Publisher-Id gegeben hat, trage sie hier ein, sonst lass das Feld bitte unverändert!", "publisherId": "Publisher-Id:" }, - "qrCode":"QR Code", + "qrCode": "QR Code", "send_gdd": "GDD versenden", "send_per_link": "GDD versenden per Link", "settings": { diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index a72a4ef25..759412019 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -164,7 +164,7 @@ "infoText": "If your referrer has given you his publisher id, enter it here, otherwise leave the field unchanged!", "publisherId": "PublisherID:" }, - "qrCode":"QR Code", + "qrCode": "QR Code", "send_gdd": "GDD send", "send_per_link": "GDD send via link", "settings": { From b2623cc83ea944ff643a3e25f4463919bcdcccec Mon Sep 17 00:00:00 2001 From: ogerly Date: Thu, 21 Apr 2022 17:46:43 +0200 Subject: [PATCH 23/68] add locales 'qrCode' --- frontend/src/locales/de.json | 1 + frontend/src/locales/en.json | 1 + 2 files changed, 2 insertions(+) diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 20ce055d4..6c891d4b2 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -164,6 +164,7 @@ "infoText": "Wenn dir dein Empfehlungsgeber seine Publisher-Id gegeben hat, trage sie hier ein, sonst lass das Feld bitte unverändert!", "publisherId": "Publisher-Id:" }, + "qrCode": "QR Code", "send_gdd": "GDD versenden", "send_per_link": "GDD versenden per Link", "settings": { diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 201c44d93..759412019 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -164,6 +164,7 @@ "infoText": "If your referrer has given you his publisher id, enter it here, otherwise leave the field unchanged!", "publisherId": "PublisherID:" }, + "qrCode": "QR Code", "send_gdd": "GDD send", "send_per_link": "GDD send via link", "settings": { From 1823bc3611293d2d7479eff67f60625044ca4bee Mon Sep 17 00:00:00 2001 From: ogerly Date: Thu, 21 Apr 2022 17:47:55 +0200 Subject: [PATCH 24/68] add methods 'showQrCodeButton', add data 'showQrcode' --- .../components/GddSend/TransactionResultLink.vue | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/GddSend/TransactionResultLink.vue b/frontend/src/components/GddSend/TransactionResultLink.vue index ee65b159a..852f8b285 100644 --- a/frontend/src/components/GddSend/TransactionResultLink.vue +++ b/frontend/src/components/GddSend/TransactionResultLink.vue @@ -4,10 +4,10 @@
{{ $t('gdd_per_link.created') }}
- +
- + {{ $t('form.close') }} @@ -33,6 +33,16 @@ export default { required: true, }, }, + data() { + return { + showQrcode: false, + } + }, + methods: { + showQrCodeButton() { + this.showQrcode = !this.showQrcode + }, + }, computed: { link() { return `${window.location.origin}/redeem/${this.code}` From 8972c05e8ad6303fb190bb32df8f6ce74ea934f2 Mon Sep 17 00:00:00 2001 From: ogerly Date: Thu, 21 Apr 2022 17:48:17 +0200 Subject: [PATCH 25/68] add emit 'showQrCodeButton' --- frontend/src/components/ClipboardCopy.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/components/ClipboardCopy.vue b/frontend/src/components/ClipboardCopy.vue index 810f73fe1..54dc67620 100644 --- a/frontend/src/components/ClipboardCopy.vue +++ b/frontend/src/components/ClipboardCopy.vue @@ -6,6 +6,9 @@ {{ $t('gdd_per_link.copy') }} + + {{ $t('qrCode') }} +
From e9767fc3dee7cc6f4dad8ebfd6e88af9295840a6 Mon Sep 17 00:00:00 2001 From: ogerly Date: Thu, 21 Apr 2022 18:02:44 +0200 Subject: [PATCH 26/68] add qr code svg in button --- frontend/src/components/ClipboardCopy.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/ClipboardCopy.vue b/frontend/src/components/ClipboardCopy.vue index 54dc67620..1e76472a4 100644 --- a/frontend/src/components/ClipboardCopy.vue +++ b/frontend/src/components/ClipboardCopy.vue @@ -6,8 +6,8 @@ {{ $t('gdd_per_link.copy') }} - - {{ $t('qrCode') }} + + From 475af6b5f1ea983a68ba3ba28d8822fd20d06ef6 Mon Sep 17 00:00:00 2001 From: ogerly Date: Thu, 21 Apr 2022 18:03:26 +0200 Subject: [PATCH 27/68] remove unused code --- frontend/src/components/ClipboardCopy.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/ClipboardCopy.vue b/frontend/src/components/ClipboardCopy.vue index 1e76472a4..753de32e0 100644 --- a/frontend/src/components/ClipboardCopy.vue +++ b/frontend/src/components/ClipboardCopy.vue @@ -7,7 +7,7 @@ {{ $t('gdd_per_link.copy') }} - + From 6360509cad72f4851c31c97677ef149a220ecd7c Mon Sep 17 00:00:00 2001 From: ogerly Date: Thu, 21 Apr 2022 18:05:04 +0200 Subject: [PATCH 28/68] fixed yarn lint --- frontend/src/locales/de.json | 1 - frontend/src/locales/en.json | 1 - 2 files changed, 2 deletions(-) diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 6c891d4b2..20ce055d4 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -164,7 +164,6 @@ "infoText": "Wenn dir dein Empfehlungsgeber seine Publisher-Id gegeben hat, trage sie hier ein, sonst lass das Feld bitte unverändert!", "publisherId": "Publisher-Id:" }, - "qrCode": "QR Code", "send_gdd": "GDD versenden", "send_per_link": "GDD versenden per Link", "settings": { diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 759412019..201c44d93 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -164,7 +164,6 @@ "infoText": "If your referrer has given you his publisher id, enter it here, otherwise leave the field unchanged!", "publisherId": "PublisherID:" }, - "qrCode": "QR Code", "send_gdd": "GDD send", "send_per_link": "GDD send via link", "settings": { From 459dc217b6397f7043b89e7537882979d7fdc187 Mon Sep 17 00:00:00 2001 From: ogerly Date: Thu, 21 Apr 2022 18:08:05 +0200 Subject: [PATCH 29/68] fixed yarn test --- frontend/src/pages/Send.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/Send.spec.js b/frontend/src/pages/Send.spec.js index 447fdde33..2b7d25ef5 100644 --- a/frontend/src/pages/Send.spec.js +++ b/frontend/src/pages/Send.spec.js @@ -249,7 +249,7 @@ describe('Send', () => { describe('close button click', () => { beforeEach(async () => { - await wrapper.findAll('button').at(1).trigger('click') + await wrapper.findAll('button').at(2).trigger('click') }) it('Shows the TransactionForm', () => { From afec1f7e59011fdb85723d2e7ae374e1d51c8339 Mon Sep 17 00:00:00 2001 From: ogerly Date: Thu, 21 Apr 2022 21:15:40 +0200 Subject: [PATCH 30/68] =?UTF-8?q?remove=20button,=20add=20dropdown=20men?= =?UTF-8?q?=C3=BC,=20change=20style?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TransactionLinks/TransactionLink.vue | 59 ++++++++----------- 1 file changed, 26 insertions(+), 33 deletions(-) diff --git a/frontend/src/components/TransactionLinks/TransactionLink.vue b/frontend/src/components/TransactionLinks/TransactionLink.vue index 66f9f2f92..abb54f8f6 100644 --- a/frontend/src/components/TransactionLinks/TransactionLink.vue +++ b/frontend/src/components/TransactionLinks/TransactionLink.vue @@ -1,49 +1,39 @@ - + {{ $t('gdd_per_link.copy') }} Date: Fri, 22 Apr 2022 15:20:46 +0200 Subject: [PATCH 54/68] changed paddings from dropdown menu --- frontend/src/components/TransactionLinks/TransactionLink.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/TransactionLinks/TransactionLink.vue b/frontend/src/components/TransactionLinks/TransactionLink.vue index e921135f8..5b8e15d5c 100644 --- a/frontend/src/components/TransactionLinks/TransactionLink.vue +++ b/frontend/src/components/TransactionLinks/TransactionLink.vue @@ -25,12 +25,12 @@ {{ $t('qrCode') }} - + {{ $t('delete') }} From fdb64071d6cff9a65198594e20b554ed01dc8616 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 25 Apr 2022 11:32:26 +0200 Subject: [PATCH 55/68] join tables to update is_admin --- database/migrations/0034-drop_server_user_table.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/0034-drop_server_user_table.ts b/database/migrations/0034-drop_server_user_table.ts index fd5a9a682..be6b44489 100644 --- a/database/migrations/0034-drop_server_user_table.ts +++ b/database/migrations/0034-drop_server_user_table.ts @@ -8,7 +8,7 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis await queryFn('ALTER TABLE `users` ADD COLUMN `is_admin` datetime DEFAULT NULL AFTER `language`;') await queryFn( - 'UPDATE `users` AS `users`, (SELECT * FROM `server_users`) AS `server_users` SET users.`is_admin` = server_users.`modified` WHERE users.`email` IN (SELECT email from `server_users`);', + 'UPDATE users AS users INNER JOIN server_users AS server_users ON users.email = server_users.email SET users.is_admin = server_users.modified WHERE users.email IN (SELECT email from server_users);', ) await queryFn('DROP TABLE `server_users`;') From d346a1d09dec91ca5981a85dc8ccb79e25c883fe Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 25 Apr 2022 11:52:10 +0200 Subject: [PATCH 56/68] test verify login --- .../src/graphql/resolver/UserResolver.test.ts | 71 ++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 07b8e59e2..edd1ec7c5 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -5,7 +5,7 @@ import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/help import { userFactory } from '@/seeds/factory/user' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { createUser, setPassword } from '@/seeds/graphql/mutations' -import { login, logout } from '@/seeds/graphql/queries' +import { login, logout, verifyLogin } from '@/seeds/graphql/queries' import { GraphQLError } from 'graphql' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { User } from '@entity/User' @@ -412,6 +412,75 @@ describe('UserResolver', () => { }) }) }) + + describe('verifyLogin', () => { + describe('unauthenticated', () => { + it('throws an error', async () => { + resetToken() + await expect(query({ query: verifyLogin })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('user exists but is not logged in', () => { + beforeAll(async () => { + await userFactory(testEnv, bibiBloxberg) + }) + + afterAll(async () => { + await cleanDB() + }) + + it('throws an error', async () => { + resetToken() + await expect(query({ query: verifyLogin })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + + describe('authenticated', () => { + const variables = { + email: 'bibi@bloxberg.de', + password: 'Aa12345_', + } + + beforeAll(async () => { + await query({ query: login, variables }) + }) + + afterAll(() => { + resetToken() + }) + + it('returns user object', async () => { + await expect(query({ query: verifyLogin })).resolves.toEqual( + expect.objectContaining({ + data: { + verifyLogin: { + email: 'bibi@bloxberg.de', + firstName: 'Bibi', + lastName: 'Bloxberg', + language: 'de', + coinanimation: true, + klickTipp: { + newsletterState: false, + }, + hasElopage: false, + publisherId: 1234, + isAdmin: false, + }, + }, + }), + ) + }) + }) + }) + }) }) describe('printTimeDuration', () => { From 276a1be889c51b6010a02562bad23ce09d697e8d Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 25 Apr 2022 12:49:23 +0200 Subject: [PATCH 57/68] test forgot password mutation --- .../src/graphql/resolver/UserResolver.test.ts | 65 ++++++++++++++++++- backend/src/seeds/graphql/mutations.ts | 6 ++ 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index edd1ec7c5..a2f5b2fdb 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -1,17 +1,18 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers' +import { testEnvironment, headerPushMock, resetToken, cleanDB, resetEntity } from '@test/helpers' import { userFactory } from '@/seeds/factory/user' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' -import { createUser, setPassword } from '@/seeds/graphql/mutations' +import { createUser, setPassword, forgotPassword } from '@/seeds/graphql/mutations' import { login, logout, verifyLogin } from '@/seeds/graphql/queries' import { GraphQLError } from 'graphql' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { User } from '@entity/User' import CONFIG from '@/config' import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' -import { printTimeDuration } from './UserResolver' +import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail' +import { printTimeDuration, activationLink } from './UserResolver' // import { klicktippSignIn } from '@/apis/KlicktippController' @@ -22,6 +23,13 @@ jest.mock('@/mailer/sendAccountActivationEmail', () => { } }) +jest.mock('@/mailer/sendResetPasswordEmail', () => { + return { + __esModule: true, + sendResetPasswordEmail: jest.fn(), + } +}) + /* jest.mock('@/apis/KlicktippController', () => { return { @@ -481,6 +489,57 @@ describe('UserResolver', () => { }) }) }) + + describe('forgotPassword', () => { + const variables = { email: 'bibi@bloxberg.de' } + describe('user is not in DB', () => { + it('returns true', async () => { + await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual( + expect.objectContaining({ + data: { + forgotPassword: true, + }, + }), + ) + }) + }) + + describe('user exists in DB', () => { + let result: any + let loginEmailOptIn: LoginEmailOptIn[] + + beforeAll(async () => { + await userFactory(testEnv, bibiBloxberg) + await resetEntity(LoginEmailOptIn) + result = await mutate({ mutation: forgotPassword, variables }) + loginEmailOptIn = await LoginEmailOptIn.find() + }) + + afterAll(async () => { + await cleanDB() + }) + + it('returns true', async () => { + await expect(result).toEqual( + expect.objectContaining({ + data: { + forgotPassword: true, + }, + }), + ) + }) + + it('sends reset password email', () => { + expect(sendResetPasswordEmail).toBeCalledWith({ + link: activationLink(loginEmailOptIn[0]), + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + duration: expect.any(String), + }) + }) + }) + }) }) describe('printTimeDuration', () => { diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index 298d56bdb..fc662cf19 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -18,6 +18,12 @@ export const setPassword = gql` } ` +export const forgotPassword = gql` + mutation ($email: String!) { + forgotPassword(email: $email) + } +` + export const updateUserInfos = gql` mutation ( $firstName: String From e30d1f723fcfa7ca078de4fe0225761f08724119 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 25 Apr 2022 13:18:08 +0200 Subject: [PATCH 58/68] test queryOptIn --- .../src/graphql/resolver/UserResolver.test.ts | 59 ++++++++++++++++++- backend/src/seeds/graphql/queries.ts | 6 ++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index a2f5b2fdb..0b62e0c99 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -5,7 +5,7 @@ import { testEnvironment, headerPushMock, resetToken, cleanDB, resetEntity } fro import { userFactory } from '@/seeds/factory/user' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { createUser, setPassword, forgotPassword } from '@/seeds/graphql/mutations' -import { login, logout, verifyLogin } from '@/seeds/graphql/queries' +import { login, logout, verifyLogin, queryOptIn } from '@/seeds/graphql/queries' import { GraphQLError } from 'graphql' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { User } from '@entity/User' @@ -538,6 +538,63 @@ describe('UserResolver', () => { duration: expect.any(String), }) }) + + describe('request reset password again', () => { + it('thows an error', async () => { + await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')], + }), + ) + }) + }) + }) + }) + + describe('queryOptIn', () => { + let loginEmailOptIn: LoginEmailOptIn[] + + beforeAll(async () => { + await userFactory(testEnv, bibiBloxberg) + loginEmailOptIn = await LoginEmailOptIn.find() + }) + + afterAll(async () => { + await cleanDB() + }) + + describe('wrong optin code', () => { + it('throws an error', async () => { + await expect( + query({ query: queryOptIn, variables: { optIn: 'not-valid' } }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + // keep Whitspace in error message! + new GraphQLError(`Could not find any entity of type "LoginEmailOptIn" matching: { + "verificationCode": "not-valid" +}`), + ], + }), + ) + }) + }) + + describe('correct optin code', () => { + it('returns true', async () => { + await expect( + query({ + query: queryOptIn, + variables: { optIn: loginEmailOptIn[0].verificationCode.toString() }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + queryOptIn: true, + }, + }), + ) + }) }) }) }) diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 11a675eeb..76a386953 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -43,6 +43,12 @@ export const logout = gql` } ` +export const queryOptIn = gql` + query ($optIn: String!) { + queryOptIn(optIn: $optIn) + } +` + export const transactionsQuery = gql` query ( $currentPage: Int = 1 From 73fe46c39d80b5f867eb015d2072b9bc44357c25 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Mon, 25 Apr 2022 14:03:38 +0200 Subject: [PATCH 59/68] also change memo column --- .../AdminPendingCreation.ts | 2 +- .../0034-admin_pending_creations_decimal.ts | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/database/entity/0034-admin_pending_creations_decimal/AdminPendingCreation.ts b/database/entity/0034-admin_pending_creations_decimal/AdminPendingCreation.ts index d204942b3..3cd83a3a5 100644 --- a/database/entity/0034-admin_pending_creations_decimal/AdminPendingCreation.ts +++ b/database/entity/0034-admin_pending_creations_decimal/AdminPendingCreation.ts @@ -16,7 +16,7 @@ export class AdminPendingCreation extends BaseEntity { @Column({ type: 'datetime', nullable: false }) date: Date - @Column({ length: 256, nullable: true, default: null }) + @Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' }) memo: string @Column({ diff --git a/database/migrations/0034-admin_pending_creations_decimal.ts b/database/migrations/0034-admin_pending_creations_decimal.ts index 6df1e563b..d3648f376 100644 --- a/database/migrations/0034-admin_pending_creations_decimal.ts +++ b/database/migrations/0034-admin_pending_creations_decimal.ts @@ -1,4 +1,7 @@ -/* MIGRATION TO CHANGE `amount` FIELD TYPE TO `Decimal` ON `admin_pending_creations` */ +/* MIGRATION TO CHANGE SEVERAL FIELDS ON `admin_pending_creations` + * - `amount` FIELD TYPE TO `Decimal` + * - `memo` FIELD TYPE TO `varchar(255)`, collate `utf8mb4_unicode_ci` + */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -18,9 +21,15 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis ) // drop `amount_bitint` column await queryFn('ALTER TABLE `admin_pending_creations` DROP COLUMN `amount_bigint`;') + + // change `memo` to varchar(255), collate utf8mb4_unicode_ci + await queryFn( + 'ALTER TABLE `admin_pending_creations` MODIFY COLUMN `memo` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL;', + ) } export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('ALTER TABLE `admin_pending_creations` MODIFY COLUMN `memo` text DEFAULT NULL;') await queryFn( 'ALTER TABLE `admin_pending_creations` ADD COLUMN `amount_bigint` bigint(20) DEFAULT NULL AFTER `amount`;', ) From 0264931e5824d4d8c087502d035bdb7b6057242c Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 25 Apr 2022 14:07:31 +0200 Subject: [PATCH 60/68] test update user infos mutation. Remove publisher ID from update user infos as it is never used --- .../src/graphql/resolver/UserResolver.test.ts | 177 +++++++++++++++++- backend/src/graphql/resolver/UserResolver.ts | 15 +- 2 files changed, 176 insertions(+), 16 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 0b62e0c99..06924699a 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -4,7 +4,7 @@ import { testEnvironment, headerPushMock, resetToken, cleanDB, resetEntity } from '@test/helpers' import { userFactory } from '@/seeds/factory/user' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' -import { createUser, setPassword, forgotPassword } from '@/seeds/graphql/mutations' +import { createUser, setPassword, forgotPassword, updateUserInfos } from '@/seeds/graphql/mutations' import { login, logout, verifyLogin, queryOptIn } from '@/seeds/graphql/queries' import { GraphQLError } from 'graphql' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' @@ -93,7 +93,7 @@ describe('UserResolver', () => { }) describe('filling all tables', () => { - it('saves the user in login_user table', () => { + it('saves the user in users table', () => { expect(user).toEqual([ { id: expect.any(Number), @@ -597,6 +597,179 @@ describe('UserResolver', () => { }) }) }) + + describe('updateUserInfos', () => { + describe('unauthenticated', () => { + it('throws an error', async () => { + resetToken() + await expect(mutate({ mutation: updateUserInfos })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + await userFactory(testEnv, bibiBloxberg) + await query({ + query: login, + variables: { + email: 'bibi@bloxberg.de', + password: 'Aa12345_', + }, + }) + }) + + afterAll(async () => { + await cleanDB() + }) + + it('returns true', async () => { + await expect(mutate({ mutation: updateUserInfos })).resolves.toEqual( + expect.objectContaining({ + data: { + updateUserInfos: true, + }, + }), + ) + }) + + describe('first-name, last-name and language', () => { + it('updates the fields in DB', async () => { + await mutate({ + mutation: updateUserInfos, + variables: { + firstName: 'Benjamin', + lastName: 'Blümchen', + locale: 'en', + }, + }) + await expect(User.findOne()).resolves.toEqual( + expect.objectContaining({ + firstName: 'Benjamin', + lastName: 'Blümchen', + language: 'en', + }), + ) + }) + }) + + describe('language is not valid', () => { + it('thows an error', async () => { + await expect( + mutate({ + mutation: updateUserInfos, + variables: { + locale: 'not-valid', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError(`"not-valid" isn't a valid language`)], + }), + ) + }) + }) + + describe('password', () => { + describe('wrong old password', () => { + it('throws an error', async () => { + await expect( + mutate({ + mutation: updateUserInfos, + variables: { + password: 'wrong password', + passwordNew: 'Aa12345_', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Old password is invalid')], + }), + ) + }) + }) + + describe('invalid new password', () => { + it('throws an error', async () => { + await expect( + mutate({ + mutation: updateUserInfos, + variables: { + password: 'Aa12345_', + passwordNew: 'Aa12345', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + new GraphQLError( + 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!', + ), + ], + }), + ) + }) + }) + + describe('correct old and new password', () => { + it('returns true', async () => { + await expect( + mutate({ + mutation: updateUserInfos, + variables: { + password: 'Aa12345_', + passwordNew: 'Bb12345_', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { updateUserInfos: true }, + }), + ) + }) + + it('can login wtih new password', async () => { + await expect( + query({ + query: login, + variables: { + email: 'bibi@bloxberg.de', + password: 'Bb12345_', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + login: expect.objectContaining({ + email: 'bibi@bloxberg.de', + }), + }, + }), + ) + }) + + it('cannot login wtih old password', async () => { + await expect( + query({ + query: login, + variables: { + email: 'bibi@bloxberg.de', + password: 'Aa12345_', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('No user with this credentials')], + }), + ) + }) + }) + }) + }) + }) }) describe('printTimeDuration', () => { diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 137c09622..a4d4f9115 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -529,15 +529,7 @@ export class UserResolver { @Mutation(() => Boolean) async updateUserInfos( @Args() - { - firstName, - lastName, - language, - publisherId, - password, - passwordNew, - coinanimation, - }: UpdateUserInfosArgs, + { firstName, lastName, language, password, passwordNew, coinanimation }: UpdateUserInfosArgs, @Ctx() context: Context, ): Promise { const userEntity = getUser(context) @@ -581,11 +573,6 @@ export class UserResolver { userEntity.privKey = encryptedPrivkey } - // Save publisherId only if Elopage is not yet registered - if (publisherId && !(await this.hasElopage(context))) { - userEntity.publisherId = publisherId - } - const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() await queryRunner.startTransaction('READ UNCOMMITTED') From 5cd2c045aa6f625a214a4f91fd9378f0740cc6bb Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 25 Apr 2022 14:08:24 +0200 Subject: [PATCH 61/68] test coverage backend to 58% --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ee602a343..3d046fcda 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -528,7 +528,7 @@ jobs: report_name: Coverage Backend type: lcov result_path: ./backend/coverage/lcov.info - min_coverage: 55 + min_coverage: 58 token: ${{ github.token }} ########################################################################## From e2957e0b345a1136ce1404a725ea7dc186a32682 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Mon, 25 Apr 2022 14:17:45 +0200 Subject: [PATCH 62/68] config value for the redeem URL was missing --- deployment/bare_metal/.env.dist | 1 + 1 file changed, 1 insertion(+) diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index 316fb60c2..a1751a859 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -21,6 +21,7 @@ WEBHOOK_GITHUB_BRANCH=master COMMUNITY_NAME="Gradido Development Stage1" COMMUNITY_URL=https://stage1.gradido.net/ COMMUNITY_REGISTER_URL=https://stage1.gradido.net/register +COMMUNITY_REDEEM_URL=https://stage1.gradido.net/redeem/{code} COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community" # backend From a4a92812bf3e21f9b0b6feb05cca3e84c1d0662d Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Mon, 25 Apr 2022 15:14:55 +0200 Subject: [PATCH 63/68] adjusted migrtion number to 35 --- .../AdminPendingCreation.ts | 0 database/entity/AdminPendingCreation.ts | 2 +- ...tions_decimal.ts => 0035-admin_pending_creations_decimal.ts} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename database/entity/{0034-admin_pending_creations_decimal => 0035-admin_pending_creations_decimal}/AdminPendingCreation.ts (100%) rename database/migrations/{0034-admin_pending_creations_decimal.ts => 0035-admin_pending_creations_decimal.ts} (100%) diff --git a/database/entity/0034-admin_pending_creations_decimal/AdminPendingCreation.ts b/database/entity/0035-admin_pending_creations_decimal/AdminPendingCreation.ts similarity index 100% rename from database/entity/0034-admin_pending_creations_decimal/AdminPendingCreation.ts rename to database/entity/0035-admin_pending_creations_decimal/AdminPendingCreation.ts diff --git a/database/entity/AdminPendingCreation.ts b/database/entity/AdminPendingCreation.ts index a32590a22..b2b37d7c4 100644 --- a/database/entity/AdminPendingCreation.ts +++ b/database/entity/AdminPendingCreation.ts @@ -1 +1 @@ -export { AdminPendingCreation } from './0034-admin_pending_creations_decimal/AdminPendingCreation' +export { AdminPendingCreation } from './0035-admin_pending_creations_decimal/AdminPendingCreation' diff --git a/database/migrations/0034-admin_pending_creations_decimal.ts b/database/migrations/0035-admin_pending_creations_decimal.ts similarity index 100% rename from database/migrations/0034-admin_pending_creations_decimal.ts rename to database/migrations/0035-admin_pending_creations_decimal.ts From a4ae002693c2a6eced4f9e066ef9d60585ab7c36 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 25 Apr 2022 16:01:26 +0200 Subject: [PATCH 64/68] round balance down before subtracting hold available amount --- backend/src/util/virtualTransactions.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/util/virtualTransactions.ts b/backend/src/util/virtualTransactions.ts index 8c1aec65b..08d44b48d 100644 --- a/backend/src/util/virtualTransactions.ts +++ b/backend/src/util/virtualTransactions.ts @@ -70,6 +70,7 @@ const virtualDecayTransaction = ( 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, From 22fcf28291f3641a1203128a4d1de353cb6458de Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 25 Apr 2022 16:06:18 +0200 Subject: [PATCH 65/68] remove strange comments --- backend/src/graphql/resolver/UserResolver.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index a4d4f9115..4fe2589ec 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -251,8 +251,6 @@ export class UserResolver { user.hasElopage = await this.hasElopage({ ...context, user: dbUser }) if (!user.hasElopage && publisherId) { user.publisherId = publisherId - // TODO: Check if we can use updateUserInfos - // await this.updateUserInfos({ publisherId }, { pubKey: loginUser.pubKey }) dbUser.publisherId = publisherId DbUser.save(dbUser) } From 54e119f88d1ad704393596058fc0029d61bb868d Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 25 Apr 2022 16:20:00 +0200 Subject: [PATCH 66/68] fix test after alter is admin field --- 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 424c2051c..c658476a4 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -481,7 +481,7 @@ describe('UserResolver', () => { }, hasElopage: false, publisherId: 1234, - isAdmin: false, + isAdmin: null, }, }, }), From 6aad14024755ddbfedc1e3ee85ac9bff2faa9751 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Mon, 25 Apr 2022 16:36:33 +0200 Subject: [PATCH 67/68] fix database version requirement for backend --- backend/src/config/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 63968c235..1eee1b9a4 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0035-admin_pending_creations_decimal.ts', + DB_VERSION: '0035-admin_pending_creations_decimal', DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0 CONFIG_VERSION: { DEFAULT: 'DEFAULT', From 84d4d41f2233211c6d454da74ee95f5b00b2f3f1 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Mon, 25 Apr 2022 16:51:48 +0200 Subject: [PATCH 68/68] v1.8.0 --- CHANGELOG.md | 46 +++++++++++++++++++++++++++++++++++++++++++ admin/package.json | 2 +- backend/package.json | 2 +- database/package.json | 2 +- frontend/package.json | 2 +- package.json | 2 +- 6 files changed, 51 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53376946c..5d8b71dac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,54 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [1.8.0](https://github.com/gradido/gradido/compare/1.7.1...1.8.0) + +- Fix: database version requirement for backend corrected [`#1835`](https://github.com/gradido/gradido/pull/1835) +- feat: More User Resolver Tests [`#1827`](https://github.com/gradido/gradido/pull/1827) +- fix: Round Decay with Tranasction Links [`#1834`](https://github.com/gradido/gradido/pull/1834) +- Fix: config value for the redeem URL was missing [`#1828`](https://github.com/gradido/gradido/pull/1828) +- Refactor: Database admin pending creations use decimal [`#1748`](https://github.com/gradido/gradido/pull/1748) +- refactor: Drop Server User Table [`#1808`](https://github.com/gradido/gradido/pull/1808) +- 1816 expired link are not highlighted [`#1821`](https://github.com/gradido/gradido/pull/1821) +- 1812 put qr code into popup on generate [`#1820`](https://github.com/gradido/gradido/pull/1820) +- Docu: Federation image [`#1817`](https://github.com/gradido/gradido/pull/1817) +- 1813 qr code popup [`#1819`](https://github.com/gradido/gradido/pull/1819) +- Fix: cross-env for windows [`#1822`](https://github.com/gradido/gradido/pull/1822) +- fix: Double Load Transaction Links [`#1818`](https://github.com/gradido/gradido/pull/1818) +- Generated link in backend should also give back the base url [`#1745`](https://github.com/gradido/gradido/pull/1745) +- 1731 style startDecayStartblock, style Adapted across pages [`#1809`](https://github.com/gradido/gradido/pull/1809) +- Refactor: Frontend bake in community info [`#1750`](https://github.com/gradido/gradido/pull/1750) +- fix: Load Transaction Link Details on Click [`#1806`](https://github.com/gradido/gradido/pull/1806) +- devops: Deploy Seed in Backend [`#1790`](https://github.com/gradido/gradido/pull/1790) +- refactor: Balance Model and Decay Rounding [`#1780`](https://github.com/gradido/gradido/pull/1780) +- change config DECAY_START_TIME in UTC 0000 [`#1807`](https://github.com/gradido/gradido/pull/1807) +- 1751 make gdt visible only if explicitly clicked [`#1752`](https://github.com/gradido/gradido/pull/1752) +- add Tab system from bootstrap in SearchUserTable Userdata [`#1744`](https://github.com/gradido/gradido/pull/1744) +- Fix: Certbot renewal [`#1789`](https://github.com/gradido/gradido/pull/1789) +- 🍰 Add Wallet Link To Mails [`#1765`](https://github.com/gradido/gradido/pull/1765) +- 1633 display qr code on link in transaction list [`#1661`](https://github.com/gradido/gradido/pull/1661) +- 1755 insert additional text when redeeming [`#1756`](https://github.com/gradido/gradido/pull/1756) +- refactor: Define Context Interface [`#1762`](https://github.com/gradido/gradido/pull/1762) +- fix: Elopage Status [`#1742`](https://github.com/gradido/gradido/pull/1742) +- Refactor: Frontend decay start block as static config value [`#1749`](https://github.com/gradido/gradido/pull/1749) +- better date format for reddem valid date [`#1758`](https://github.com/gradido/gradido/pull/1758) +- add insert shadow in summary links transaction type [`#1754`](https://github.com/gradido/gradido/pull/1754) +- Feature: JWT duration is now 30min by default [`#1747`](https://github.com/gradido/gradido/pull/1747) +- Docu: Scope of Gradido [`#1746`](https://github.com/gradido/gradido/pull/1746) +- fix: Check That Recipient User Has Activated Account to Receive Coins [`#1743`](https://github.com/gradido/gradido/pull/1743) +- Fix: Fixed config dist version to properly reflect new password reset url [`#1737`](https://github.com/gradido/gradido/pull/1737) +- 503 transaction list pagination pages clickable [`#1677`](https://github.com/gradido/gradido/pull/1677) +- if no recipientEmail else form.email [`#1722`](https://github.com/gradido/gradido/pull/1722) +- 1727 change button text and observe spelling [`#1728`](https://github.com/gradido/gradido/pull/1728) +- 1729 load spinner if pending balance [`#1730`](https://github.com/gradido/gradido/pull/1730) +- transaction type remains when jumping from the verification back [`#1724`](https://github.com/gradido/gradido/pull/1724) +- text for toast expand link copied [`#1726`](https://github.com/gradido/gradido/pull/1726) + #### [1.7.1](https://github.com/gradido/gradido/compare/1.7.0...1.7.1) +> 1 April 2022 + +- v1.7.1 [`#1721`](https://github.com/gradido/gradido/pull/1721) - fix: Localize Dates on Redeem Transaction Link Page [`#1720`](https://github.com/gradido/gradido/pull/1720) - fix: Round Virtual Transaction Link Transaction [`#1718`](https://github.com/gradido/gradido/pull/1718) - larger icon and deacy information if center [`#1719`](https://github.com/gradido/gradido/pull/1719) diff --git a/admin/package.json b/admin/package.json index 3d3919954..c5b2e60f5 100644 --- a/admin/package.json +++ b/admin/package.json @@ -3,7 +3,7 @@ "description": "Administraion Interface for Gradido", "main": "index.js", "author": "Moriz Wahl", - "version": "1.7.1", + "version": "1.8.0", "license": "MIT", "private": false, "scripts": { diff --git a/backend/package.json b/backend/package.json index 8654f4cc7..c5318d674 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "gradido-backend", - "version": "1.7.1", + "version": "1.8.0", "description": "Gradido unified backend providing an API-Service for Gradido Transactions", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/backend", diff --git a/database/package.json b/database/package.json index a1fffa882..13c638c79 100644 --- a/database/package.json +++ b/database/package.json @@ -1,6 +1,6 @@ { "name": "gradido-database", - "version": "1.7.1", + "version": "1.8.0", "description": "Gradido Database Tool to execute database migrations", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/database", diff --git a/frontend/package.json b/frontend/package.json index b3091d4b4..18021e705 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "bootstrap-vue-gradido-wallet", - "version": "1.7.1", + "version": "1.8.0", "private": true, "scripts": { "start": "node run/server.js", diff --git a/package.json b/package.json index 2ff37c71a..c8abef594 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gradido", - "version": "1.7.1", + "version": "1.8.0", "description": "Gradido", "main": "index.js", "repository": "git@github.com:gradido/gradido.git",