From 6aa8ae9bf4edadb56fa319caaed08e7e9bcb0221 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 16 May 2023 04:02:26 +0200 Subject: [PATCH 1/4] feat(backend): send coins via alias --- backend/src/graphql/resolver/TransactionResolver.ts | 1 + backend/src/graphql/resolver/util/findUserByIdentifier.ts | 8 +++++++- backend/src/graphql/resolver/util/validateAlias.ts | 6 +++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 3c540b1f6..04f636b5a 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -323,6 +323,7 @@ export class TransactionResolver { } // TODO this is subject to replay attacks + // --- WHY? const senderUser = getUser(context) // validate recipient user diff --git a/backend/src/graphql/resolver/util/findUserByIdentifier.ts b/backend/src/graphql/resolver/util/findUserByIdentifier.ts index df932e544..dd4f9a775 100644 --- a/backend/src/graphql/resolver/util/findUserByIdentifier.ts +++ b/backend/src/graphql/resolver/util/findUserByIdentifier.ts @@ -4,6 +4,8 @@ import { validate, version } from 'uuid' import { LogError } from '@/server/LogError' +import { validAliasRegex } from './validateAlias' + export const findUserByIdentifier = async (identifier: string): Promise => { let user: DbUser | undefined if (validate(identifier) && version(identifier) === 4) { @@ -27,8 +29,12 @@ export const findUserByIdentifier = async (identifier: string): Promise } user = userContact.user user.emailContact = userContact + } else if (validAliasRegex.exec(identifier)) { + user = await DbUser.findOne({ where: { alias: identifier }, relations: ['emailContact'] }) + if (!user) { + throw new LogError('No user found to given identifier', identifier) + } } else { - // last is alias when implemented throw new LogError('Unknown identifier type', identifier) } diff --git a/backend/src/graphql/resolver/util/validateAlias.ts b/backend/src/graphql/resolver/util/validateAlias.ts index dcea7824c..3afc9c7d0 100644 --- a/backend/src/graphql/resolver/util/validateAlias.ts +++ b/backend/src/graphql/resolver/util/validateAlias.ts @@ -3,6 +3,8 @@ import { User as DbUser } from '@entity/User' import { LogError } from '@/server/LogError' +export const validAliasRegex = /^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9])*$/ + const reservedAlias = [ 'admin', 'email', @@ -24,9 +26,7 @@ const reservedAlias = [ export const validateAlias = async (alias: string): Promise => { if (alias.length < 3) throw new LogError('Given alias is too short', alias) if (alias.length > 20) throw new LogError('Given alias is too long', alias) - /* eslint-disable-next-line security/detect-unsafe-regex */ - if (!alias.match(/^[0-9A-Za-z]([_-]?[A-Za-z0-9])+$/)) - throw new LogError('Invalid characters in alias', alias) + if (!alias.match(validAliasRegex)) throw new LogError('Invalid characters in alias', alias) if (reservedAlias.includes(alias.toLowerCase())) throw new LogError('Alias is not allowed', alias) const aliasInUse = await DbUser.find({ where: { alias: Raw((a) => `LOWER(${a}) = "${alias.toLowerCase()}"`) }, From 2e865c6744c619bab3a41d5d3448ae5c7d52dde0 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 16 May 2023 04:38:19 +0200 Subject: [PATCH 2/4] test send coins via alias --- .../resolver/TransactionResolver.test.ts | 116 +++++++++++++++++- 1 file changed, 114 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index 24fa4e48c..96d434a29 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -19,6 +19,7 @@ import { createContribution, login, sendCoins, + updateUserInfos, } from '@/seeds/graphql/mutations' import { transactionsQuery } from '@/seeds/graphql/queries' import { bobBaumeister } from '@/seeds/users/bob-baumeister' @@ -52,10 +53,13 @@ let bobData: any let peterData: any let user: User[] +let bob: User +let peter: User + describe('send coins', () => { beforeAll(async () => { - await userFactory(testEnv, peterLustig) - await userFactory(testEnv, bobBaumeister) + peter = await userFactory(testEnv, peterLustig) + bob = await userFactory(testEnv, bobBaumeister) await userFactory(testEnv, stephenHawking) await userFactory(testEnv, garrickOllivander) @@ -376,6 +380,114 @@ describe('send coins', () => { }) }) + describe('send coins via gradido ID', () => { + it('sends the coins', async () => { + await expect( + mutate({ + mutation: sendCoins, + variables: { + identifier: peter?.gradidoID, + amount: 10, + memo: 'send via gradido ID', + }, + }), + ).resolves.toMatchObject({ + data: { + sendCoins: true, + }, + errors: undefined, + }) + }) + }) + + describe('send coins via alias', () => { + beforeAll(async () => { + await mutate({ + mutation: updateUserInfos, + variables: { + alias: 'bob', + }, + }) + await mutate({ + mutation: login, + variables: peterData, + }) + }) + + afterAll(async () => { + await mutate({ + mutation: login, + variables: bobData, + }) + }) + + it('sends the coins', async () => { + await expect( + mutate({ + mutation: sendCoins, + variables: { + identifier: 'bob', + amount: 6.66, + memo: 'send via alias', + }, + }), + ).resolves.toMatchObject({ + data: { + sendCoins: true, + }, + errors: undefined, + }) + }) + + describe("peter's transactions", () => { + it('has all expected transactions', async () => { + await expect(query({ query: transactionsQuery })).resolves.toMatchObject({ + data: { + transactionList: { + balance: expect.any(Object), + transactions: [ + expect.objectContaining({ + typeId: 'DECAY', + }), + expect.objectContaining({ + amount: expect.decimalEqual(-6.66), + linkedUser: { + firstName: 'Bob', + gradidoID: bob?.gradidoID, + lastName: 'der Baumeister', + }, + memo: 'send via alias', + typeId: 'SEND', + }), + expect.objectContaining({ + amount: expect.decimalEqual(10), + linkedUser: { + firstName: 'Bob', + gradidoID: bob?.gradidoID, + lastName: 'der Baumeister', + }, + memo: 'send via gradido ID', + typeId: 'RECEIVE', + }), + expect.objectContaining({ + amount: expect.decimalEqual(50), + linkedUser: { + firstName: 'Bob', + gradidoID: bob?.gradidoID, + lastName: 'der Baumeister', + }, + memo: 'unrepeatable memo', + typeId: 'RECEIVE', + }), + ], + }, + }, + errors: undefined, + }) + }) + }) + }) + describe('more transactions to test semaphore', () => { it('sends the coins four times in a row', async () => { await expect( From 1fbebddd7174b5a37ade4623629a184d4d401c35 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 16 May 2023 05:00:06 +0200 Subject: [PATCH 3/4] more tests for transactions and finding users by different identifiers --- backend/jest.config.js | 2 +- .../src/graphql/resolver/UserResolver.test.ts | 38 +++++++++++++++++-- .../graphql/resolver/util/validateAlias.ts | 3 +- frontend/src/validation-rules.js | 2 +- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/backend/jest.config.js b/backend/jest.config.js index 3b251916a..81ebbec55 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -7,7 +7,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], coverageThreshold: { global: { - lines: 86, + lines: 89, }, }, setupFiles: ['/test/testSetup.ts'], diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index f46d0a9bc..25787490f 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -2358,15 +2358,21 @@ describe('UserResolver', () => { mutation: login, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, }) + await mutate({ + mutation: updateUserInfos, + variables: { + alias: 'bibi', + }, + }) }) - describe('identifier is no gradido ID and no email', () => { + describe('identifier is no gradido ID, no email and no alias', () => { it('throws and logs "Unknown identifier type" error', async () => { await expect( query({ query: userQuery, variables: { - identifier: 'identifier', + identifier: 'identifier_is_no_valid_alias!', }, }), ).resolves.toEqual( @@ -2374,7 +2380,10 @@ describe('UserResolver', () => { errors: [new GraphQLError('Unknown identifier type')], }), ) - expect(logger.error).toBeCalledWith('Unknown identifier type', 'identifier') + expect(logger.error).toBeCalledWith( + 'Unknown identifier type', + 'identifier_is_no_valid_alias!', + ) }) }) @@ -2441,6 +2450,29 @@ describe('UserResolver', () => { ) }) }) + + describe('identifier is found via alias', () => { + it('returns user', async () => { + await expect( + query({ + query: userQuery, + variables: { + identifier: 'bibi', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + user: { + firstName: 'Bibi', + lastName: 'Bloxberg', + }, + }, + errors: undefined, + }), + ) + }) + }) }) }) diff --git a/backend/src/graphql/resolver/util/validateAlias.ts b/backend/src/graphql/resolver/util/validateAlias.ts index 3afc9c7d0..88cb9b982 100644 --- a/backend/src/graphql/resolver/util/validateAlias.ts +++ b/backend/src/graphql/resolver/util/validateAlias.ts @@ -3,7 +3,8 @@ import { User as DbUser } from '@entity/User' import { LogError } from '@/server/LogError' -export const validAliasRegex = /^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9])*$/ +// eslint-disable-next-line security/detect-unsafe-regex +export const validAliasRegex = /^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/ const reservedAlias = [ 'admin', diff --git a/frontend/src/validation-rules.js b/frontend/src/validation-rules.js index 53b301676..0c30b96ee 100644 --- a/frontend/src/validation-rules.js +++ b/frontend/src/validation-rules.js @@ -141,7 +141,7 @@ export const loadAllRules = (i18nCallback, apollo) => { extend('usernameUnique', { validate(value) { - if (value.match(/^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9])*$/)) { + if (value.match(/^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/)) { return apollo .query({ query: checkUsername, From 85bfcd9df3bde90dbfb5112eb2de8ed9af6e9e86 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 17 May 2023 16:24:05 +0200 Subject: [PATCH 4/4] constants to upper case --- .../src/graphql/resolver/util/findUserByIdentifier.ts | 4 ++-- backend/src/graphql/resolver/util/validateAlias.ts | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/backend/src/graphql/resolver/util/findUserByIdentifier.ts b/backend/src/graphql/resolver/util/findUserByIdentifier.ts index dd4f9a775..bd9a25071 100644 --- a/backend/src/graphql/resolver/util/findUserByIdentifier.ts +++ b/backend/src/graphql/resolver/util/findUserByIdentifier.ts @@ -4,7 +4,7 @@ import { validate, version } from 'uuid' import { LogError } from '@/server/LogError' -import { validAliasRegex } from './validateAlias' +import { VALID_ALIAS_REGEX } from './validateAlias' export const findUserByIdentifier = async (identifier: string): Promise => { let user: DbUser | undefined @@ -29,7 +29,7 @@ export const findUserByIdentifier = async (identifier: string): Promise } user = userContact.user user.emailContact = userContact - } else if (validAliasRegex.exec(identifier)) { + } else if (VALID_ALIAS_REGEX.exec(identifier)) { user = await DbUser.findOne({ where: { alias: identifier }, relations: ['emailContact'] }) if (!user) { throw new LogError('No user found to given identifier', identifier) diff --git a/backend/src/graphql/resolver/util/validateAlias.ts b/backend/src/graphql/resolver/util/validateAlias.ts index 88cb9b982..721733be4 100644 --- a/backend/src/graphql/resolver/util/validateAlias.ts +++ b/backend/src/graphql/resolver/util/validateAlias.ts @@ -4,9 +4,9 @@ import { User as DbUser } from '@entity/User' import { LogError } from '@/server/LogError' // eslint-disable-next-line security/detect-unsafe-regex -export const validAliasRegex = /^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/ +export const VALID_ALIAS_REGEX = /^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/ -const reservedAlias = [ +const RESERVED_ALIAS = [ 'admin', 'email', 'gast', @@ -27,8 +27,9 @@ const reservedAlias = [ export const validateAlias = async (alias: string): Promise => { if (alias.length < 3) throw new LogError('Given alias is too short', alias) if (alias.length > 20) throw new LogError('Given alias is too long', alias) - if (!alias.match(validAliasRegex)) throw new LogError('Invalid characters in alias', alias) - if (reservedAlias.includes(alias.toLowerCase())) throw new LogError('Alias is not allowed', alias) + if (!alias.match(VALID_ALIAS_REGEX)) throw new LogError('Invalid characters in alias', alias) + if (RESERVED_ALIAS.includes(alias.toLowerCase())) + throw new LogError('Alias is not allowed', alias) const aliasInUse = await DbUser.find({ where: { alias: Raw((a) => `LOWER(${a}) = "${alias.toLowerCase()}"`) }, })