From 924db68d0638172d3dbc280e1ca7dd321d9abed6 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 7 Jul 2022 15:52:55 +0200 Subject: [PATCH 01/22] setup community statistics --- backend/src/auth/RIGHTS.ts | 1 + .../src/graphql/model/CommunityStatistics.ts | 26 +++++++++++++++++++ .../graphql/resolver/StatisticsResolver.ts | 26 +++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 backend/src/graphql/model/CommunityStatistics.ts create mode 100644 backend/src/graphql/resolver/StatisticsResolver.ts diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index c10fc96de..cbf650ec1 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -44,4 +44,5 @@ export enum RIGHTS { LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS', DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK', UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK', + COMMUNITY_STATISTICS = 'COMMUNITY_STATISTICS', } diff --git a/backend/src/graphql/model/CommunityStatistics.ts b/backend/src/graphql/model/CommunityStatistics.ts new file mode 100644 index 000000000..61354115c --- /dev/null +++ b/backend/src/graphql/model/CommunityStatistics.ts @@ -0,0 +1,26 @@ +import { ObjectType, Field } from 'type-graphql' +import Decimal from 'decimal.js-light' + +@ObjectType() +export class CommunityStatistics { + @Field(() => Number) + totalUsers: number + + @Field(() => Number) + activeUsers: number + + @Field(() => Number) + deletedUsers: number + + @Field(() => Decimal) + totalGradidoCreated: Decimal + + @Field(() => Decimal) + totalGradidoDecayed: Decimal + + @Field(() => Decimal) + totalGradidoAvailable: Decimal + + @Field(() => Decimal) + totalGradidoUnbookedDecayed: Decimal +} diff --git a/backend/src/graphql/resolver/StatisticsResolver.ts b/backend/src/graphql/resolver/StatisticsResolver.ts new file mode 100644 index 000000000..a90d42f75 --- /dev/null +++ b/backend/src/graphql/resolver/StatisticsResolver.ts @@ -0,0 +1,26 @@ +import { Resolver, Query, Arg, Args, Authorized, Ctx, Int } from 'type-graphql' +import { RIGHTS } from '@/auth/RIGHTS' +import { CommunityStatistics } from '@model/CommunityStatistics' +import { User as DbUser } from '@entity/User' +import { getConnection } from '@dbTools/typeorm' +import Decimal from 'decimal.js-light' + +@Resolver() +export class StatisticsResolver { + @Authorized([RIGHTS.COMMUNITY_STATISTICS]) + @Query(() => CommunityStatistics) + async communityStatistics(): Promise { + const totalUsers = await DbUser.find({ withDeleted: true }) + console.log(totalUsers.length) + + return { + totalUsers: 12, + activeUsers: 6, + deletedUsers: 1, + totalGradidoCreated: new Decimal(3000), + totalGradidoDecayed: new Decimal(200), + totalGradidoAvailable: new Decimal(2800), + totalGradidoUnbookedDecayed: new Decimal(200), + } + } +} From df227607ad539ad0bc209eebed917d0a7b356bed Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 7 Jul 2022 17:27:50 +0200 Subject: [PATCH 02/22] get basic statistics from database --- .../graphql/resolver/StatisticsResolver.ts | 71 ++++++++++++++++--- 1 file changed, 61 insertions(+), 10 deletions(-) diff --git a/backend/src/graphql/resolver/StatisticsResolver.ts b/backend/src/graphql/resolver/StatisticsResolver.ts index a90d42f75..4c1500839 100644 --- a/backend/src/graphql/resolver/StatisticsResolver.ts +++ b/backend/src/graphql/resolver/StatisticsResolver.ts @@ -1,26 +1,77 @@ -import { Resolver, Query, Arg, Args, Authorized, Ctx, Int } from 'type-graphql' +import { Resolver, Query, Authorized } from 'type-graphql' import { RIGHTS } from '@/auth/RIGHTS' import { CommunityStatistics } from '@model/CommunityStatistics' import { User as DbUser } from '@entity/User' +import { Transaction as DbTransaction } from '@entity/Transaction' import { getConnection } from '@dbTools/typeorm' import Decimal from 'decimal.js-light' +import { calculateDecay } from '@/util/decay' @Resolver() export class StatisticsResolver { @Authorized([RIGHTS.COMMUNITY_STATISTICS]) @Query(() => CommunityStatistics) async communityStatistics(): Promise { - const totalUsers = await DbUser.find({ withDeleted: true }) - console.log(totalUsers.length) + const allUsers = await DbUser.find({ withDeleted: true }) + + let totalUsers = 0 + let activeUsers = 0 + let deletedUsers = 0 + + let totalGradidoAvailable: Decimal = new Decimal(0) + let totalGradidoUnbookedDecayed: Decimal = new Decimal(0) + + const receivedCallDate = new Date() + + for (let i = 0; i < allUsers.length; i++) { + if (allUsers[i].deletedAt) { + deletedUsers++ + } else { + totalUsers++ + const lastTransaction = await DbTransaction.findOne({ + where: { userId: allUsers[i].id }, + order: { balanceDate: 'DESC' }, + }) + if (lastTransaction) { + activeUsers++ + const decay = calculateDecay( + lastTransaction.balance, + lastTransaction.balanceDate, + receivedCallDate, + ) + if (decay) { + totalGradidoAvailable = totalGradidoAvailable.plus(decay.balance.toString()) + totalGradidoUnbookedDecayed = totalGradidoUnbookedDecayed.plus(decay.decay.toString()) + } + } + } + } + + const queryRunner = getConnection().createQueryRunner() + await queryRunner.connect() + + const { totalGradidoCreated } = await queryRunner.manager + .createQueryBuilder() + .select('SUM(transaction.amount) AS totalGradidoCreated') + .from(DbTransaction, 'transaction') + .where('transaction.typeId = 1') + .getRawOne() + + const { totalGradidoDecayed } = await queryRunner.manager + .createQueryBuilder() + .select('SUM(transaction.decay) AS totalGradidoDecayed') + .from(DbTransaction, 'transaction') + .where('transaction.decay IS NOT NULL') + .getRawOne() return { - totalUsers: 12, - activeUsers: 6, - deletedUsers: 1, - totalGradidoCreated: new Decimal(3000), - totalGradidoDecayed: new Decimal(200), - totalGradidoAvailable: new Decimal(2800), - totalGradidoUnbookedDecayed: new Decimal(200), + totalUsers, + activeUsers, + deletedUsers, + totalGradidoCreated, + totalGradidoDecayed, + totalGradidoAvailable, + totalGradidoUnbookedDecayed, } } } From baa5084f5d6365485f1af3a109e5dcd771155eed Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 7 Jul 2022 17:29:09 +0200 Subject: [PATCH 03/22] add query for communty statistics --- admin/src/graphql/communityStatistics.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 admin/src/graphql/communityStatistics.js diff --git a/admin/src/graphql/communityStatistics.js b/admin/src/graphql/communityStatistics.js new file mode 100644 index 000000000..868bfd02a --- /dev/null +++ b/admin/src/graphql/communityStatistics.js @@ -0,0 +1,15 @@ +import gql from 'graphql-tag' + +export const communityStatistics = gql` + query { + communityStatistics { + totalUsers + activeUsers + deletedUsers + totalGradidoCreated + totalGradidoDecayed + totalGradidoAvailable + totalGradidoUnbookedDecayed + } + } +` From 7686c805466fede1c033bd231cf682fb70d46b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 9 Aug 2022 20:11:58 +0200 Subject: [PATCH 04/22] install uuid package --- database/package.json | 4 +++- database/yarn.lock | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/database/package.json b/database/package.json index 05e1c2ac8..837e61438 100644 --- a/database/package.json +++ b/database/package.json @@ -37,6 +37,7 @@ "typescript": "^4.3.5" }, "dependencies": { + "@types/uuid": "^8.3.4", "cross-env": "^7.0.3", "crypto": "^1.0.1", "decimal.js-light": "^2.5.1", @@ -44,6 +45,7 @@ "mysql2": "^2.3.0", "reflect-metadata": "^0.1.13", "ts-mysql-migrate": "^1.0.2", - "typeorm": "^0.2.38" + "typeorm": "^0.2.38", + "uuid": "^8.3.2" } } diff --git a/database/yarn.lock b/database/yarn.lock index e5d74929c..b30db4595 100644 --- a/database/yarn.lock +++ b/database/yarn.lock @@ -137,6 +137,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.10.3.tgz#7a8f2838603ea314d1d22bb3171d899e15c57bd5" integrity sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ== +"@types/uuid@^8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + "@types/zen-observable@0.8.3": version "0.8.3" resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.3.tgz#781d360c282436494b32fe7d9f7f8e64b3118aa3" @@ -2088,6 +2093,11 @@ util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" From 37c5015079965c5882d13c1c1d79c4caa6eefb75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 9 Aug 2022 20:50:10 +0200 Subject: [PATCH 05/22] add gradidoId in users table --- .../User.ts | 111 ++++++++++++++++++ database/entity/User.ts | 2 +- .../0045-adapt_users_table_for_gradidoid.ts | 44 +++++++ 3 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 database/entity/0045-adapt_users_table_for_gradidoid/User.ts create mode 100644 database/migrations/0045-adapt_users_table_for_gradidoid.ts diff --git a/database/entity/0045-adapt_users_table_for_gradidoid/User.ts b/database/entity/0045-adapt_users_table_for_gradidoid/User.ts new file mode 100644 index 000000000..3f2547cad --- /dev/null +++ b/database/entity/0045-adapt_users_table_for_gradidoid/User.ts @@ -0,0 +1,111 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + DeleteDateColumn, + OneToMany, + JoinColumn, +} from 'typeorm' +import { Contribution } from '../Contribution' + +@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) +export class User extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ + name: 'gradido_id', + length: 36, + nullable: false, + unique: true, + collation: 'utf8mb4_unicode_ci', + }) + gradidoID: string + + @Column({ + name: 'alias', + length: 20, + nullable: true, + unique: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + alias: string + + @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: 'datetime', nullable: true, default: null }) + isAdmin: Date | null + + @Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null }) + referrerId?: number | null + + @Column({ + name: 'contribution_link_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + contributionLinkId?: number | null + + @Column({ name: 'publisher_id', default: 0 }) + publisherId: number + + @Column({ + type: 'text', + name: 'passphrase', + collation: 'utf8mb4_unicode_ci', + nullable: true, + default: null, + }) + passphrase: string + + @OneToMany(() => Contribution, (contribution) => contribution.user) + @JoinColumn({ name: 'user_id' }) + contributions?: Contribution[] +} diff --git a/database/entity/User.ts b/database/entity/User.ts index 99b8c8ca9..89b5d3d7f 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0040-add_contribution_link_id_to_user/User' +export { User } from './0045-adapt_users_table_for_gradidoid/User' diff --git a/database/migrations/0045-adapt_users_table_for_gradidoid.ts b/database/migrations/0045-adapt_users_table_for_gradidoid.ts new file mode 100644 index 000000000..8e8372efa --- /dev/null +++ b/database/migrations/0045-adapt_users_table_for_gradidoid.ts @@ -0,0 +1,44 @@ +/* MIGRATION TO ADD GRADIDO_ID + * + * This migration adds new columns to the table `users` and creates the + * new table `user_contacts` + */ + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { v4 as uuidv4 } from 'uuid' + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + // First add gradido_id as nullable column without Default + await queryFn('ALTER TABLE `users` ADD COLUMN `gradido_id` CHAR(36) NULL AFTER `id`;') + + // Second update gradido_id with ensured unique uuidv4 + const usersToUpdate = await queryFn('SELECT `id`, `gradido_id` FROM `users`') // WHERE 'u.gradido_id' is null`,) + for (const id in usersToUpdate) { + const user = usersToUpdate[id] + let gradidoId = null + let countIds = null + do { + gradidoId = uuidv4() + countIds = await queryFn( + `SELECT COUNT(*) FROM \`users\` WHERE \`gradido_id\` = "${gradidoId}"`, + ) + } while (countIds[0] > 0) + await queryFn( + `UPDATE \`users\` SET \`gradido_id\` = "${gradidoId}" WHERE \`id\` = "${user.id}"`, + ) + } + + // third modify gradido_id to not nullable and unique + await queryFn('ALTER TABLE `users` MODIFY COLUMN `gradido_id` CHAR(36) NOT NULL UNIQUE;') + + await queryFn( + 'ALTER TABLE `users` ADD COLUMN `alias` varchar(20) NULL UNIQUE AFTER `gradido_id`;', + ) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('ALTER TABLE users DROP COLUMN gradido_id;') + await queryFn('ALTER TABLE users DROP COLUMN alias;') +} From 14229edf5993a7bd762b08865d84c974840d2364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 9 Aug 2022 20:52:07 +0200 Subject: [PATCH 06/22] install uuid package --- backend/package.json | 4 +++- backend/yarn.lock | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/package.json b/backend/package.json index c56c8e960..d31d12eda 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,6 +19,7 @@ "dependencies": { "@types/jest": "^27.0.2", "@types/lodash.clonedeep": "^4.5.6", + "@types/uuid": "^8.3.4", "apollo-server-express": "^2.25.2", "apollo-server-testing": "^2.25.2", "axios": "^0.21.1", @@ -39,7 +40,8 @@ "reflect-metadata": "^0.1.13", "sodium-native": "^3.3.0", "ts-jest": "^27.0.5", - "type-graphql": "^1.1.1" + "type-graphql": "^1.1.1", + "uuid": "^8.3.2" }, "devDependencies": { "@types/express": "^4.17.12", diff --git a/backend/yarn.lock b/backend/yarn.lock index 53a53cb9b..dd84e2ce5 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1000,6 +1000,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/uuid@^8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + "@types/validator@^13.1.3": version "13.6.3" resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.6.3.tgz#31ca2e997bf13a0fffca30a25747d5b9f7dbb7de" @@ -5437,7 +5442,7 @@ uuid@^3.1.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.0.0: +uuid@^8.0.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== From 95332546184127fa09a2e2e083daaa5bd0382b1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 9 Aug 2022 23:33:58 +0200 Subject: [PATCH 07/22] upgrade DB-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 f44aa584c..677090ee2 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0044-insert_missing_contributions', + DB_VERSION: '0045-adapt_users_table_for_gradidoid', 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 35da60c572fa5000a702536917b87cb605633ca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 9 Aug 2022 23:36:00 +0200 Subject: [PATCH 08/22] change User to create a gradidoID as uuidv4 during createUser --- backend/src/graphql/model/User.ts | 8 ++++++++ backend/src/graphql/resolver/UserResolver.ts | 21 +++++++++++++++++++- backend/src/util/communityUser.ts | 2 ++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index 0642be630..1ec33b27d 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -8,6 +8,8 @@ import { FULL_CREATION_AVAILABLE } from '../resolver/const/const' export class User { constructor(user: dbUser, creation: Decimal[] = FULL_CREATION_AVAILABLE) { this.id = user.id + this.gradidoID = user.gradidoID + this.alias = user.alias this.email = user.email this.firstName = user.firstName this.lastName = user.lastName @@ -28,6 +30,12 @@ export class User { // `public_key` binary(32) DEFAULT NULL, // `privkey` binary(80) DEFAULT NULL, + @Field(() => String) + gradidoID: string + + @Field(() => String, { nullable: true }) + alias: string + // TODO privacy issue here @Field(() => String) email: string diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index a89a8cb0b..03ed3043e 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -32,6 +32,8 @@ import { EventSendConfirmationEmail, } from '@/event/Event' import { getUserCreation } from './util/creations' +import { v4 as uuidv4 } from 'uuid' + // eslint-disable-next-line @typescript-eslint/no-var-requires const sodium = require('sodium-native') @@ -224,6 +226,19 @@ export const activationLink = (optInCode: LoginEmailOptIn): string => { return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, optInCode.verificationCode.toString()) } +const newGradidoID = async (): Promise => { + let gradidoId: string + let countIds: number + do { + gradidoId = uuidv4() + countIds = await DbUser.count({ where: { gradidoID: gradidoId } }) + if (countIds > 0) { + logger.info('Gradido-ID creation conflict...') + } + } while (countIds > 0) + return gradidoId +} + @Resolver() export class UserResolver { @Authorized([RIGHTS.VERIFY_LOGIN]) @@ -344,11 +359,13 @@ export class UserResolver { logger.info(`DbUser.findOne(email=${email}) = ${userFound}`) if (userFound) { - logger.info('User already exists with this email=' + email) + // ATTENTION: this logger-message will be exactly expected during tests + logger.info(`User already exists with this email=${email}`) // TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent. 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 = '11111111-2222-4333-4444-55555555' user.email = email user.firstName = firstName user.lastName = lastName @@ -378,11 +395,13 @@ export class UserResolver { // const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash // const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1]) const emailHash = getEmailHash(email) + const gradidoID = await newGradidoID() const eventRegister = new EventRegister() const eventRedeemRegister = new EventRedeemRegister() const eventSendConfirmEmail = new EventSendConfirmationEmail() const dbUser = new DbUser() + dbUser.gradidoID = gradidoID dbUser.email = email dbUser.firstName = firstName dbUser.lastName = lastName diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index 1a84c2cdf..d850c5382 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -6,6 +6,8 @@ import { User } from '@model/User' const communityDbUser: dbUser = { id: -1, + gradidoID: '11111111-2222-4333-4444-55555555', + alias: '', email: 'support@gradido.net', firstName: 'Gradido', lastName: 'Akademie', From 2b4e94ef66502c1bbcca1e9ebb7a71837ff70c86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 9 Aug 2022 23:41:39 +0200 Subject: [PATCH 09/22] add gradidoID-Test during createUser --- backend/src/graphql/resolver/UserResolver.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index a2a499224..39ff70b22 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 { TransactionLink } from '@entity/TransactionLink' import { logger } from '@test/testSetup' +import { validate as validateUUID, version as versionUUID } from 'uuid' // import { klicktippSignIn } from '@/apis/KlicktippController' @@ -111,6 +112,8 @@ describe('UserResolver', () => { expect(user).toEqual([ { id: expect.any(Number), + gradidoID: expect.any(String), + alias: null, email: 'peter@lustig.de', firstName: 'Peter', lastName: 'Lustig', @@ -129,6 +132,10 @@ describe('UserResolver', () => { contributionLinkId: null, }, ]) + const valUUID = validateUUID(user[0].gradidoID) + const verUUID = versionUUID(user[0].gradidoID) + expect(valUUID).toEqual(true) + expect(verUUID).toEqual(4) }) it('creates an email optin', () => { From cd71bf76617c8a5aebbef1528dac7e934f165557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 9 Aug 2022 23:59:13 +0200 Subject: [PATCH 10/22] linting --- backend/src/graphql/resolver/UserResolver.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 03ed3043e..d9245f51d 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -34,7 +34,6 @@ import { import { getUserCreation } from './util/creations' import { v4 as uuidv4 } from 'uuid' - // eslint-disable-next-line @typescript-eslint/no-var-requires const sodium = require('sodium-native') // eslint-disable-next-line @typescript-eslint/no-var-requires From d0d4b151dc9451925d6ab928817bf730b1efd223 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 10 Aug 2022 11:27:05 +0200 Subject: [PATCH 11/22] Add rights and roles to query admins and moderators. --- backend/src/auth/RIGHTS.ts | 1 + backend/src/auth/ROLES.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index d5e2cc7ce..bb722246a 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -30,6 +30,7 @@ export enum RIGHTS { LIST_CONTRIBUTIONS = 'LIST_CONTRIBUTIONS', LIST_ALL_CONTRIBUTIONS = 'LIST_ALL_CONTRIBUTIONS', UPDATE_CONTRIBUTION = 'UPDATE_CONTRIBUTION', + SEARCH_ADMIN_USERS = 'SEARCH_ADMIN_USERS', // Admin SEARCH_USERS = 'SEARCH_USERS', SET_USER_ROLE = 'SET_USER_ROLE', diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts index 9dcba0a4b..7d8025ab9 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -28,6 +28,7 @@ export const ROLE_USER = new Role('user', [ RIGHTS.LIST_CONTRIBUTIONS, RIGHTS.LIST_ALL_CONTRIBUTIONS, RIGHTS.UPDATE_CONTRIBUTION, + RIGHTS.SEARCH_ADMIN_USERS, ]) export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights From 304732bdade175a4ed56784e5904b327c97488c2 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 10 Aug 2022 11:27:27 +0200 Subject: [PATCH 12/22] Add model for AdminUser --- backend/src/graphql/model/AdminUser.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 backend/src/graphql/model/AdminUser.ts diff --git a/backend/src/graphql/model/AdminUser.ts b/backend/src/graphql/model/AdminUser.ts new file mode 100644 index 000000000..8ddca69a0 --- /dev/null +++ b/backend/src/graphql/model/AdminUser.ts @@ -0,0 +1,25 @@ +import { User } from "@entity/User" +import { Field, Int, ObjectType } from "type-graphql" + +@ObjectType() +export class AdminUser { + constructor(user: User) { + this.firstName = user.firstName + this.lastName = user.lastName + } + + @Field(() => String) + firstName: string + + @Field(() => String) + lastName: string +} + +@ObjectType() +export class SearchAdminUsersResult { + @Field(() => Int) + userCount: number + + @Field(() => [AdminUser]) + userList: AdminUser[] +} From c46f2f241ec6d78f4c593aca4c042337d8e6f35d Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 10 Aug 2022 11:28:21 +0200 Subject: [PATCH 13/22] Create resolver method to query admin and moderator users. --- backend/src/graphql/resolver/UserResolver.ts | 46 +++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index a89a8cb0b..b54651a2b 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -3,7 +3,7 @@ import { backendLogger as logger } from '@/server/logger' import { Context, getUser } from '@/server/context' import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql' -import { getConnection } from '@dbTools/typeorm' +import { getConnection, getCustomRepository, IsNull, Not } from '@dbTools/typeorm' import CONFIG from '@/config' import { User } from '@model/User' import { User as DbUser } from '@entity/User' @@ -32,6 +32,10 @@ import { EventSendConfirmationEmail, } from '@/event/Event' import { getUserCreation } from './util/creations' +import { UserRepository } from '@/typeorm/repository/User' +import { SearchAdminUsersResult } from '@model/AdminUser' +import Paginated from '@arg/Paginated' +import { Order } from '@enum/Order' // eslint-disable-next-line @typescript-eslint/no-var-requires const sodium = require('sodium-native') @@ -731,6 +735,46 @@ export class UserResolver { logger.debug(`has ElopageBuys = ${elopageBuys}`) return elopageBuys } + + @Authorized([RIGHTS.SEARCH_ADMIN_USERS]) + @Query(() => SearchAdminUsersResult) + async searchAdminUsers( + @Args() + { currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated, + ): Promise { + const userRepository = getCustomRepository(UserRepository) + + const [users, count] = await userRepository.findAndCount({ + where: { + isAdmin: Not(IsNull()), + }, + order: { + createdAt: order, + }, + skip: (currentPage - 1) * pageSize, + take: pageSize, + }) + + if (users.length === 0) { + return { + userCount: 0, + userList: [], + } + } + + const adminUsers = await Promise.all( + users.map((user) => { + return { + firstName: user.firstName, + lastName: user.lastName, + } + }), + ) + return { + userCount: count, + userList: adminUsers, + } + } } const isTimeExpired = (optIn: LoginEmailOptIn, duration: number): boolean => { From d3238fd48666a0c9083a47642eb658b1a8f7646c Mon Sep 17 00:00:00 2001 From: elweyn Date: Mon, 15 Aug 2022 11:05:11 +0200 Subject: [PATCH 14/22] Fix linting. --- backend/src/auth/RIGHTS.ts | 1 - backend/src/auth/ROLES.ts | 3 --- backend/src/graphql/model/AdminUser.ts | 4 ++-- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index 934efd866..0152e89df 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -32,7 +32,6 @@ export enum RIGHTS { UPDATE_CONTRIBUTION = 'UPDATE_CONTRIBUTION', LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS', SEARCH_ADMIN_USERS = 'SEARCH_ADMIN_USERS', - // Admin SEARCH_USERS = 'SEARCH_USERS', SET_USER_ROLE = 'SET_USER_ROLE', diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts index 87bb46bac..434dc11a4 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -28,11 +28,8 @@ export const ROLE_USER = new Role('user', [ RIGHTS.LIST_CONTRIBUTIONS, RIGHTS.LIST_ALL_CONTRIBUTIONS, RIGHTS.UPDATE_CONTRIBUTION, -<<<<<<< HEAD RIGHTS.SEARCH_ADMIN_USERS, -======= RIGHTS.LIST_CONTRIBUTION_LINKS, ->>>>>>> master ]) export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights diff --git a/backend/src/graphql/model/AdminUser.ts b/backend/src/graphql/model/AdminUser.ts index 8ddca69a0..92a22b7f1 100644 --- a/backend/src/graphql/model/AdminUser.ts +++ b/backend/src/graphql/model/AdminUser.ts @@ -1,5 +1,5 @@ -import { User } from "@entity/User" -import { Field, Int, ObjectType } from "type-graphql" +import { User } from '@entity/User' +import { Field, Int, ObjectType } from 'type-graphql' @ObjectType() export class AdminUser { From e876f32ad529ad63632f0faef309dab3d65724d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 16 Aug 2022 17:05:31 +0200 Subject: [PATCH 15/22] initialize gradidoID of fakedUser per uuidv4() instead of constant --- 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 d9245f51d..a8967e963 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -364,7 +364,7 @@ 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 = '11111111-2222-4333-4444-55555555' + user.gradidoID = uuidv4() user.email = email user.firstName = firstName user.lastName = lastName From beb563ac5cb72578094a5a1ede0ee1eb5da02fa9 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 17 Aug 2022 10:05:56 +0200 Subject: [PATCH 16/22] Fix changes. --- backend/src/graphql/resolver/UserResolver.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 0368d7a31..09790f846 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -793,7 +793,12 @@ export class UserResolver { ) return { userCount: count, - userList: adminUsers, + userList: users.map((user) => { + return { + firstName: user.firstName, + lastName: user.lastName, + } + }), } } } From a44bcc00e22b43e533799f3e78a60bfd48410ef1 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 17 Aug 2022 10:09:12 +0200 Subject: [PATCH 17/22] Remove unused code. --- backend/src/graphql/resolver/UserResolver.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 09790f846..ff11c841e 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -776,21 +776,6 @@ export class UserResolver { take: pageSize, }) - if (users.length === 0) { - return { - userCount: 0, - userList: [], - } - } - - const adminUsers = await Promise.all( - users.map((user) => { - return { - firstName: user.firstName, - lastName: user.lastName, - } - }), - ) return { userCount: count, userList: users.map((user) => { From f96fb5aa638647eeaabc9f760d669e27a2478e58 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 17 Aug 2022 10:21:40 +0200 Subject: [PATCH 18/22] Add searchAdminUsers query to mocks. --- backend/src/seeds/graphql/queries.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 9f7a02e70..3bd042ac2 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -280,3 +280,15 @@ export const listContributionLinks = gql` } } ` + +export const searchAdminUsers = gql` + query { + searchAdminUsers { + userCount + userList { + firstName + lastName + } + } + } +` From 6c707eae59990be6aa32bc39658daf8c3d911fe3 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 17 Aug 2022 10:22:08 +0200 Subject: [PATCH 19/22] Start tests for searchAdminUsers. --- backend/src/graphql/resolver/UserResolver.test.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 35a569c4b..fdfc31467 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, updateUserInfos } from '@/seeds/graphql/mutations' -import { login, logout, verifyLogin, queryOptIn } from '@/seeds/graphql/queries' +import { login, logout, verifyLogin, queryOptIn, searchAdminUsers } from '@/seeds/graphql/queries' import { GraphQLError } from 'graphql' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { User } from '@entity/User' @@ -878,6 +878,19 @@ bei Gradidio sei dabei!`, }) }) }) + + describe('searchAdminUsers', () => { + describe('unauthenticated', () => { + it('throws an error', async () => { + resetToken() + await expect(mutate({ mutation: searchAdminUsers })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + }) }) describe('printTimeDuration', () => { From 1c301d34a2637f727697def347e64c9e0ca1a7e5 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 17 Aug 2022 11:37:13 +0200 Subject: [PATCH 20/22] Test for searchAdminUsers. --- .../src/graphql/resolver/UserResolver.test.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index fdfc31467..c7727ca5f 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -21,6 +21,7 @@ import { ContributionLink } from '@model/ContributionLink' import { logger } from '@test/testSetup' import { validate as validateUUID, version as versionUUID } from 'uuid' +import { peterLustig } from '@/seeds/users/peter-lustig' // import { klicktippSignIn } from '@/apis/KlicktippController' @@ -890,6 +891,38 @@ bei Gradidio sei dabei!`, ) }) }) + + describe('authenticated', () => { + beforeAll(async () => { + await userFactory(testEnv, bibiBloxberg) + await userFactory(testEnv, peterLustig) + await query({ + query: login, + variables: { + email: 'bibi@bloxberg.de', + password: 'Aa12345_', + }, + }) + }) + + it('finds peter@lustig.de', async () => { + await expect(mutate({ mutation: searchAdminUsers })).resolves.toEqual( + expect.objectContaining({ + data: { + searchAdminUsers: { + userCount: 1, + userList: expect.arrayContaining([ + expect.objectContaining({ + firstName: 'Peter', + lastName: 'Lustig', + }), + ]), + }, + }, + }), + ) + }) + }) }) }) From b1a17c52ecdf6be3923f82fd31ba401190605581 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 17 Aug 2022 12:46:29 +0200 Subject: [PATCH 21/22] Add community_statistics to user ROLES. --- backend/src/auth/ROLES.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts index 500c8bec4..25dc3dc92 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -29,6 +29,7 @@ export const ROLE_USER = new Role('user', [ RIGHTS.LIST_ALL_CONTRIBUTIONS, RIGHTS.UPDATE_CONTRIBUTION, RIGHTS.LIST_CONTRIBUTION_LINKS, + RIGHTS.COMMUNITY_STATISTICS, ]) export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights From 194d4a6ad2c9ddb3f3664d047c7c2a638f1d518f Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 17 Aug 2022 15:15:35 +0200 Subject: [PATCH 22/22] move right COMMUNITY_STATISTICS to user rights --- backend/src/auth/RIGHTS.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index a58d0aa2b..6e1c1e63b 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -31,6 +31,7 @@ export enum RIGHTS { LIST_ALL_CONTRIBUTIONS = 'LIST_ALL_CONTRIBUTIONS', UPDATE_CONTRIBUTION = 'UPDATE_CONTRIBUTION', LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS', + COMMUNITY_STATISTICS = 'COMMUNITY_STATISTICS', // Admin SEARCH_USERS = 'SEARCH_USERS', SET_USER_ROLE = 'SET_USER_ROLE', @@ -48,5 +49,4 @@ export enum RIGHTS { CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK', DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK', UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK', - COMMUNITY_STATISTICS = 'COMMUNITY_STATISTICS', }