diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index a77840738..25e901491 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -12,7 +12,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0072-add_communityuuid_to_transactions_table', + DB_VERSION: '0073-introduce_foreign_user_in_users_table', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info diff --git a/backend/src/federation/client/1_0/model/SendCoinsArgs.ts b/backend/src/federation/client/1_0/model/SendCoinsArgs.ts index fb97da925..ae9a01f58 100644 --- a/backend/src/federation/client/1_0/model/SendCoinsArgs.ts +++ b/backend/src/federation/client/1_0/model/SendCoinsArgs.ts @@ -26,4 +26,7 @@ export class SendCoinsArgs { @Field(() => String) senderUserName: string + + @Field(() => String, { nullable: true }) + senderAlias?: string | null } diff --git a/backend/src/graphql/arg/TransactionSendArgs.ts b/backend/src/graphql/arg/TransactionSendArgs.ts index 48827be8d..5bd8b89f7 100644 --- a/backend/src/graphql/arg/TransactionSendArgs.ts +++ b/backend/src/graphql/arg/TransactionSendArgs.ts @@ -7,9 +7,9 @@ import { IsPositiveDecimal } from '@/graphql/validator/Decimal' @ArgsType() export class TransactionSendArgs { - @Field(() => String, { nullable: true }) + @Field(() => String) @IsString() - recipientCommunityIdentifier?: string | null | undefined + recipientCommunityIdentifier: string @Field(() => String) @IsString() diff --git a/backend/src/graphql/arg/UserArgs.ts b/backend/src/graphql/arg/UserArgs.ts new file mode 100644 index 000000000..633ed5e20 --- /dev/null +++ b/backend/src/graphql/arg/UserArgs.ts @@ -0,0 +1,13 @@ +import { IsString } from 'class-validator' +import { ArgsType, Field } from 'type-graphql' + +@ArgsType() +export class UserArgs { + @Field({ nullable: false }) + @IsString() + identifier: string + + @Field({ nullable: true }) + @IsString() + communityIdentifier?: string +} diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index 3474c6901..cd188f49f 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -8,6 +8,8 @@ export class User { constructor(user: dbUser | null) { if (user) { this.id = user.id + this.foreign = user.foreign + this.communityUuid = user.communityUuid this.gradidoID = user.gradidoID this.alias = user.alias if (user.emailContact) { @@ -30,6 +32,15 @@ export class User { @Field(() => Int) id: number + @Field(() => Boolean) + foreign: boolean + + @Field(() => String) + communityUuid: string + + @Field(() => String, { nullable: true }) + communityName: string | null + @Field(() => String) gradidoID: string diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index 857159a97..d130a802e 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -13,6 +13,7 @@ import { Transaction } from '@entity/Transaction' import { User } from '@entity/User' import { ApolloServerTestClient } from 'apollo-server-testing' import { GraphQLError } from 'graphql' +import { v4 as uuidv4 } from 'uuid' import { cleanDB, testEnvironment } from '@test/helpers' import { logger } from '@test/testSetup' @@ -71,12 +72,8 @@ let fedForeignCom: DbFederatedCommunity describe('send coins', () => { beforeAll(async () => { - peter = await userFactory(testEnv, peterLustig) - bob = await userFactory(testEnv, bobBaumeister) - await userFactory(testEnv, stephenHawking) - await userFactory(testEnv, garrickOllivander) homeCom = DbCommunity.create() - homeCom.communityUuid = '7f474922-b6d8-4b64-8cd0-ebf0a1d875aa' + homeCom.communityUuid = uuidv4() homeCom.creationDate = new Date('2000-01-01') homeCom.description = 'homeCom description' homeCom.foreign = false @@ -87,7 +84,7 @@ describe('send coins', () => { homeCom = await DbCommunity.save(homeCom) foreignCom = DbCommunity.create() - foreignCom.communityUuid = '7f474922-b6d8-4b64-8cd0-cea0a1d875bb' + foreignCom.communityUuid = uuidv4() foreignCom.creationDate = new Date('2000-06-06') foreignCom.description = 'foreignCom description' foreignCom.foreign = true @@ -98,6 +95,11 @@ describe('send coins', () => { foreignCom.authenticatedAt = new Date('2000-06-12') foreignCom = await DbCommunity.save(foreignCom) + peter = await userFactory(testEnv, peterLustig) + bob = await userFactory(testEnv, bobBaumeister) + await userFactory(testEnv, stephenHawking) + await userFactory(testEnv, garrickOllivander) + bobData = { email: 'bob@baumeister.de', password: 'Aa12345_', diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 66a889798..8d35708a6 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -38,7 +38,7 @@ import { calculateBalance } from '@/util/validate' import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions' import { BalanceResolver } from './BalanceResolver' -import { isCommunityAuthenticated, isHomeCommunity } from './util/communities' +import { getCommunity, getCommunityName, isHomeCommunity } from './util/communities' import { findUserByIdentifier } from './util/findUserByIdentifier' import { getLastTransaction } from './util/getLastTransaction' import { getTransactionList } from './util/getTransactionList' @@ -47,6 +47,7 @@ import { processXComPendingSendCoins, } from './util/processXComSendCoins' import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector' +import { storeForeignUser } from './util/storeForeignUser' import { transactionLinkSummary } from './util/transactionLinkSummary' export const executeTransaction = async ( @@ -250,18 +251,50 @@ export class TransactionResolver { // find involved users; I am involved const involvedUserIds: number[] = [user.id] const involvedRemoteUsers: User[] = [] - userTransactions.forEach((transaction: dbTransaction) => { + // userTransactions.forEach((transaction: dbTransaction) => { + // use normal for loop because of timing problems with await in forEach-loop + for (const transaction of userTransactions) { if (transaction.linkedUserId && !involvedUserIds.includes(transaction.linkedUserId)) { involvedUserIds.push(transaction.linkedUserId) } if (!transaction.linkedUserId && transaction.linkedUserGradidoID) { - const remoteUser = new User(null) - remoteUser.gradidoID = transaction.linkedUserGradidoID - remoteUser.firstName = transaction.linkedUserName - remoteUser.lastName = '(GradidoID: ' + transaction.linkedUserGradidoID + ')' + logger.debug( + 'search for remoteUser...', + transaction.linkedUserCommunityUuid, + transaction.linkedUserGradidoID, + ) + const dbRemoteUser = await dbUser.findOne({ + where: [ + { + foreign: true, + communityUuid: transaction.linkedUserCommunityUuid ?? undefined, + gradidoID: transaction.linkedUserGradidoID, + }, + ], + }) + logger.debug('found dbRemoteUser:', dbRemoteUser) + const remoteUser = new User(dbRemoteUser) + if (dbRemoteUser === null) { + logger.debug('no dbRemoteUser found, init from tx:', transaction) + if (transaction.linkedUserCommunityUuid !== null) { + remoteUser.communityUuid = transaction.linkedUserCommunityUuid + } + remoteUser.gradidoID = transaction.linkedUserGradidoID + if (transaction.linkedUserName) { + remoteUser.firstName = transaction.linkedUserName.slice( + 0, + transaction.linkedUserName.indexOf(' '), + ) + remoteUser.lastName = transaction.linkedUserName?.slice( + transaction.linkedUserName.indexOf(' '), + transaction.linkedUserName.length, + ) + } + } + remoteUser.communityName = await getCommunityName(remoteUser.communityUuid) involvedRemoteUsers.push(remoteUser) } - }) + } logger.debug(`involvedUserIds=`, involvedUserIds) logger.debug(`involvedRemoteUsers=`, involvedRemoteUsers) @@ -336,7 +369,7 @@ export class TransactionResolver { (userTransactions.length && userTransactions[0].balance) || new Decimal(0), ), ) - logger.debug(`transactions=${transactions}`) + logger.debug(`transactions=`, transactions) } } @@ -363,7 +396,7 @@ export class TransactionResolver { } transactions.push(new Transaction(userTransaction, self, linkedUser)) }) - logger.debug(`TransactionTypeId.CREATION: transactions=${transactions}`) + logger.debug(`TransactionTypeId.CREATION: transactions=`, transactions) transactions.forEach((transaction: Transaction) => { if (transaction.typeId !== TransactionTypeId.DECAY) { @@ -393,11 +426,16 @@ export class TransactionResolver { if (!recipientCommunityIdentifier || (await isHomeCommunity(recipientCommunityIdentifier))) { // processing sendCoins within sender and recepient are both in home community - // validate recipient user - const recipientUser = await findUserByIdentifier(recipientIdentifier) + const recipientUser = await findUserByIdentifier( + recipientIdentifier, + recipientCommunityIdentifier, + ) if (!recipientUser) { throw new LogError('The recipient user was not found', recipientUser) } + if (recipientUser.foreign) { + throw new LogError('Found foreign recipient user for a local transaction:', recipientUser) + } await executeTransaction(amount, memo, senderUser, recipientUser) logger.info('successful executeTransaction', amount, memo, senderUser, recipientUser) @@ -407,13 +445,17 @@ export class TransactionResolver { if (!CONFIG.FEDERATION_XCOM_SENDCOINS_ENABLED) { throw new LogError('X-Community sendCoins disabled per configuration!') } - if (!(await isCommunityAuthenticated(recipientCommunityIdentifier))) { + const recipCom = await getCommunity(recipientCommunityIdentifier) + logger.debug('recipient commuity: ', recipCom) + if (recipCom === null) { + throw new LogError( + 'no recipient commuity found for identifier:', + recipientCommunityIdentifier, + ) + } + if (recipCom !== null && recipCom.authenticatedAt === null) { throw new LogError('recipient commuity is connected, but still not authenticated yet!') } - const recipCom = await DbCommunity.findOneOrFail({ - where: { communityUuid: recipientCommunityIdentifier }, - }) - logger.debug('recipient commuity: ', recipCom) let pendingResult: SendCoinsResult let committingResult: SendCoinsResult const creationDate = new Date() @@ -438,7 +480,7 @@ export class TransactionResolver { amount, memo, senderUser, - pendingResult.recipGradidoID, + pendingResult, ) logger.debug('processXComCommittingSendCoins result: ', committingResult) if (!committingResult.vote) { @@ -451,6 +493,15 @@ export class TransactionResolver { memo, ) } + // after successful x-com-tx store the recipient as foreign user + logger.debug('store recipient as foreign user...') + if (await storeForeignUser(recipCom, committingResult)) { + logger.info( + 'X-Com: new foreign user inserted successfully...', + recipCom.communityUuid, + committingResult.recipGradidoID, + ) + } } } catch (err) { throw new LogError( diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 171af7cf5..a9c50553e 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -5,6 +5,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ import { Connection } from '@dbTools/typeorm' +import { Community as DbCommunity } from '@entity/Community' import { Event as DbEvent } from '@entity/Event' import { TransactionLink } from '@entity/TransactionLink' import { User } from '@entity/User' @@ -171,6 +172,8 @@ describe('UserResolver', () => { referrerId: null, contributionLinkId: null, passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD, + communityUuid: null, + foreign: false, }, ]) const valUUID = validateUUID(user[0].gradidoID) @@ -2500,6 +2503,39 @@ describe('UserResolver', () => { }) describe('user', () => { + let homeCom1: DbCommunity + let foreignCom1: DbCommunity + + beforeAll(async () => { + homeCom1 = DbCommunity.create() + homeCom1.foreign = false + homeCom1.url = 'http://localhost/api' + homeCom1.publicKey = Buffer.from('publicKey-HomeCommunity') + homeCom1.privateKey = Buffer.from('privateKey-HomeCommunity') + homeCom1.communityUuid = uuidv4() // 'HomeCom-UUID' + homeCom1.authenticatedAt = new Date() + homeCom1.name = 'HomeCommunity-name' + homeCom1.description = 'HomeCommunity-description' + homeCom1.creationDate = new Date() + await DbCommunity.insert(homeCom1) + + foreignCom1 = DbCommunity.create() + foreignCom1.foreign = true + foreignCom1.url = 'http://stage-2.gradido.net/api' + foreignCom1.publicKey = Buffer.from('publicKey-stage-2_Community') + foreignCom1.privateKey = Buffer.from('privateKey-stage-2_Community') + foreignCom1.communityUuid = uuidv4() // 'Stage2-Com-UUID' + foreignCom1.authenticatedAt = new Date() + foreignCom1.name = 'Stage-2_Community-name' + foreignCom1.description = 'Stage-2_Community-description' + foreignCom1.creationDate = new Date() + await DbCommunity.insert(foreignCom1) + }) + + afterAll(async () => { + await DbCommunity.clear() + }) + beforeEach(() => { jest.clearAllMocks() }) @@ -2546,6 +2582,7 @@ describe('UserResolver', () => { query: userQuery, variables: { identifier: 'identifier_is_no_valid_alias!', + communityIdentifier: homeCom1.communityUuid, }, }), ).resolves.toEqual( @@ -2567,14 +2604,44 @@ describe('UserResolver', () => { query: userQuery, variables: { identifier: uuid, + communityIdentifier: homeCom1.communityUuid, }, }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('No user found to given identifier')], + errors: [new GraphQLError('No user found to given identifier(s)')], }), ) - expect(logger.error).toBeCalledWith('No user found to given identifier', uuid) + expect(logger.error).toBeCalledWith( + 'No user found to given identifier(s)', + uuid, + homeCom1.communityUuid, + ) + }) + }) + + describe('identifier is found via email, but not matching community', () => { + it('returns user', async () => { + await expect( + query({ + query: userQuery, + variables: { + identifier: 'bibi@bloxberg.de', + communityIdentifier: foreignCom1.communityUuid, + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + new GraphQLError('Found user to given contact, but belongs to other community'), + ], + }), + ) + expect(logger.error).toBeCalledWith( + 'Found user to given contact, but belongs to other community', + 'bibi@bloxberg.de', + foreignCom1.communityUuid, + ) }) }) @@ -2585,15 +2652,16 @@ describe('UserResolver', () => { query: userQuery, variables: { identifier: 'bibi@bloxberg.de', + communityIdentifier: homeCom1.communityUuid, }, }), ).resolves.toEqual( expect.objectContaining({ data: { - user: { + user: expect.objectContaining({ firstName: 'Bibi', lastName: 'Bloxberg', - }, + }), }, errors: undefined, }), @@ -2608,15 +2676,16 @@ describe('UserResolver', () => { query: userQuery, variables: { identifier: user.gradidoID, + communityIdentifier: homeCom1.communityUuid, }, }), ).resolves.toEqual( expect.objectContaining({ data: { - user: { + user: expect.objectContaining({ firstName: 'Bibi', lastName: 'Bloxberg', - }, + }), }, errors: undefined, }), @@ -2631,15 +2700,16 @@ describe('UserResolver', () => { query: userQuery, variables: { identifier: 'bibi', + communityIdentifier: homeCom1.communityUuid, }, }), ).resolves.toEqual( expect.objectContaining({ data: { - user: { + user: expect.objectContaining({ firstName: 'Bibi', lastName: 'Bloxberg', - }, + }), }, errors: undefined, }), diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 6408b9dda..665340e63 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -12,6 +12,7 @@ import i18n from 'i18n' import { Resolver, Query, Args, Arg, Authorized, Ctx, Mutation, Int } from 'type-graphql' import { v4 as uuidv4 } from 'uuid' +import { UserArgs } from '@arg//UserArgs' import { CreateUserArgs } from '@arg/CreateUserArgs' import { Paginated } from '@arg/Paginated' import { SearchUsersFilters } from '@arg/SearchUsersFilters' @@ -64,6 +65,7 @@ import random from 'random-bigint' import { randombytes_random } from 'sodium-native' import { FULL_CREATION_AVAILABLE } from './const/const' +import { getCommunityName, getHomeCommunity } from './util/communities' import { getUserCreations } from './util/creations' import { findUserByIdentifier } from './util/findUserByIdentifier' import { findUsers } from './util/findUsers' @@ -809,8 +811,18 @@ export class UserResolver { @Authorized([RIGHTS.USER]) @Query(() => User) - async user(@Arg('identifier') identifier: string): Promise { - return new User(await findUserByIdentifier(identifier)) + async user( + @Args() + { identifier, communityIdentifier }: UserArgs, + ): Promise { + const foundDbUser = await findUserByIdentifier(identifier, communityIdentifier) + const modelUser = new User(foundDbUser) + if (!foundDbUser.communityUuid) { + modelUser.communityName = (await Promise.resolve(getHomeCommunity())).name + } else { + modelUser.communityName = await getCommunityName(foundDbUser.communityUuid) + } + return modelUser } } diff --git a/backend/src/graphql/resolver/util/communities.ts b/backend/src/graphql/resolver/util/communities.ts index 5854f94b0..0c0023a19 100644 --- a/backend/src/graphql/resolver/util/communities.ts +++ b/backend/src/graphql/resolver/util/communities.ts @@ -15,6 +15,12 @@ export async function isHomeCommunity(communityIdentifier: string): Promise { + return await DbCommunity.findOneOrFail({ + where: [{ foreign: false }], + }) +} + export async function getCommunityUrl(communityIdentifier: string): Promise { const community = await DbCommunity.findOneOrFail({ where: [ diff --git a/backend/src/graphql/resolver/util/findUserByIdentifier.ts b/backend/src/graphql/resolver/util/findUserByIdentifier.ts index 96c9eb458..6b7f1bccc 100644 --- a/backend/src/graphql/resolver/util/findUserByIdentifier.ts +++ b/backend/src/graphql/resolver/util/findUserByIdentifier.ts @@ -6,12 +6,18 @@ import { LogError } from '@/server/LogError' import { VALID_ALIAS_REGEX } from './validateAlias' -export const findUserByIdentifier = async (identifier: string): Promise => { +export const findUserByIdentifier = async ( + identifier: string, + communityIdentifier?: string, +): Promise => { let user: DbUser | null if (validate(identifier) && version(identifier) === 4) { - user = await DbUser.findOne({ where: { gradidoID: identifier }, relations: ['emailContact'] }) + user = await DbUser.findOne({ + where: { gradidoID: identifier, communityUuid: communityIdentifier }, + relations: ['emailContact'], + }) if (!user) { - throw new LogError('No user found to given identifier', identifier) + throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier) } } else if (/^.{2,}@.{2,}\..{2,}$/.exec(identifier)) { const userContact = await DbUserContact.findOne({ @@ -27,12 +33,22 @@ export const findUserByIdentifier = async (identifier: string): Promise if (!userContact.user) { throw new LogError('No user to given contact', identifier) } + if (userContact.user.communityUuid !== communityIdentifier) { + throw new LogError( + 'Found user to given contact, but belongs to other community', + identifier, + communityIdentifier, + ) + } user = userContact.user user.emailContact = userContact } else if (VALID_ALIAS_REGEX.exec(identifier)) { - user = await DbUser.findOne({ where: { alias: identifier }, relations: ['emailContact'] }) + user = await DbUser.findOne({ + where: { alias: identifier, communityUuid: communityIdentifier }, + relations: ['emailContact'], + }) if (!user) { - throw new LogError('No user found to given identifier', identifier) + throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier) } } else { throw new LogError('Unknown identifier type', identifier) diff --git a/backend/src/graphql/resolver/util/processXComSendCoins.ts b/backend/src/graphql/resolver/util/processXComSendCoins.ts index af6826bd1..6455bc36d 100644 --- a/backend/src/graphql/resolver/util/processXComSendCoins.ts +++ b/backend/src/graphql/resolver/util/processXComSendCoins.ts @@ -85,6 +85,7 @@ export async function processXComPendingSendCoins( } args.senderUserUuid = sender.gradidoID args.senderUserName = fullName(sender.firstName, sender.lastName) + args.senderAlias = sender.alias logger.debug(`X-Com: ready for voteForSendCoins with args=`, args) voteResult = await client.voteForSendCoins(args) logger.debug(`X-Com: returned from voteForSendCoins:`, voteResult) @@ -158,7 +159,7 @@ export async function processXComCommittingSendCoins( amount: Decimal, memo: string, sender: dbUser, - recipUuid: string, + recipient: SendCoinsResult, ): Promise { const sendCoinsResult = new SendCoinsResult() try { @@ -170,7 +171,7 @@ export async function processXComCommittingSendCoins( amount, memo, sender, - recipUuid, + recipient, ) // first find pending Tx with given parameters const pendingTx = await DbPendingTransaction.findOneBy({ @@ -179,7 +180,7 @@ export async function processXComCommittingSendCoins( userName: fullName(sender.firstName, sender.lastName), linkedUserCommunityUuid: receiverCom.communityUuid ?? CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID, - linkedUserGradidoID: recipUuid, + linkedUserGradidoID: recipient.recipGradidoID ? recipient.recipGradidoID : undefined, typeId: TransactionTypeId.SEND, state: PendingTransactionState.NEW, balanceDate: creationDate, @@ -212,6 +213,7 @@ export async function processXComCommittingSendCoins( if (pendingTx.userName) { args.senderUserName = pendingTx.userName } + args.senderAlias = sender.alias logger.debug(`X-Com: ready for settleSendCoins with args=`, args) const acknowledge = await client.settleSendCoins(args) logger.debug(`X-Com: returnd from settleSendCoins:`, acknowledge) @@ -235,6 +237,7 @@ export async function processXComCommittingSendCoins( ) } sendCoinsResult.recipGradidoID = pendingTx.linkedUserGradidoID + sendCoinsResult.recipAlias = recipient.recipAlias } } catch (err) { logger.error(`Error in writing sender pending transaction: `, err) diff --git a/backend/src/graphql/resolver/util/storeForeignUser.ts b/backend/src/graphql/resolver/util/storeForeignUser.ts new file mode 100644 index 000000000..5942159b4 --- /dev/null +++ b/backend/src/graphql/resolver/util/storeForeignUser.ts @@ -0,0 +1,75 @@ +import { Community as DbCommunity } from '@entity/Community' +import { User as DbUser } from '@entity/User' + +import { SendCoinsResult } from '@/federation/client/1_0/model/SendCoinsResult' +import { backendLogger as logger } from '@/server/logger' + +export async function storeForeignUser( + recipCom: DbCommunity, + committingResult: SendCoinsResult, +): Promise { + if (recipCom.communityUuid !== null && committingResult.recipGradidoID !== null) { + try { + const user = await DbUser.findOne({ + where: { + foreign: true, + communityUuid: recipCom.communityUuid, + gradidoID: committingResult.recipGradidoID, + }, + }) + if (!user) { + logger.debug( + 'X-Com: no foreignUser found for:', + recipCom.communityUuid, + committingResult.recipGradidoID, + ) + let foreignUser = DbUser.create() + foreignUser.foreign = true + if (committingResult.recipAlias !== null) { + foreignUser.alias = committingResult.recipAlias + } + foreignUser.communityUuid = recipCom.communityUuid + if (committingResult.recipFirstName !== null) { + foreignUser.firstName = committingResult.recipFirstName + } + if (committingResult.recipLastName !== null) { + foreignUser.lastName = committingResult.recipLastName + } + foreignUser.gradidoID = committingResult.recipGradidoID + foreignUser = await DbUser.save(foreignUser) + logger.debug('X-Com: new foreignUser inserted:', foreignUser) + + return true + } else if ( + user.firstName !== committingResult.recipFirstName || + user.lastName !== committingResult.recipLastName || + user.alias !== committingResult.recipAlias + ) { + logger.warn( + 'X-Com: foreignUser still exists, but with different name or alias:', + user, + committingResult, + ) + if (committingResult.recipFirstName !== null) { + user.firstName = committingResult.recipFirstName + } + if (committingResult.recipLastName !== null) { + user.lastName = committingResult.recipLastName + } + if (committingResult.recipAlias !== null) { + user.alias = committingResult.recipAlias + } + await DbUser.save(user) + logger.debug('update recipient successful.', user) + return true + } else { + logger.debug('X-Com: foreignUser still exists...:', user) + return true + } + } catch (err) { + logger.error('X-Com: error in storeForeignUser;', err) + return false + } + } + return false +} diff --git a/backend/src/seeds/factory/user.ts b/backend/src/seeds/factory/user.ts index 3fa5591a2..65b0ff3bb 100644 --- a/backend/src/seeds/factory/user.ts +++ b/backend/src/seeds/factory/user.ts @@ -5,6 +5,7 @@ import { ApolloServerTestClient } from 'apollo-server-testing' import { RoleNames } from '@enum/RoleNames' +import { getHomeCommunity } from '@/graphql/resolver/util/communities' import { setUserRole } from '@/graphql/resolver/util/modifyUserRole' import { createUser, setPassword } from '@/seeds/graphql/mutations' import { UserInterface } from '@/seeds/users/UserInterface' @@ -43,6 +44,15 @@ export const userFactory = async ( } await dbUser.save() } + try { + const homeCom = await getHomeCommunity() + if (homeCom.communityUuid) { + dbUser.communityUuid = homeCom.communityUuid + await User.save(dbUser) + } + } catch (err) { + // no homeCommunity exists + } // get last changes of user from database dbUser = await User.findOneOrFail({ diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index b7ef87aa8..4bd5008d3 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -370,10 +370,14 @@ export const adminListContributionMessages = gql` ` export const user = gql` - query ($identifier: String!) { - user(identifier: $identifier) { + query ($identifier: String!, $communityIdentifier: String) { + user(identifier: $identifier, communityIdentifier: $communityIdentifier) { firstName lastName + foreign + communityUuid + gradidoID + alias } } ` diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index 422c836df..bad06f201 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -33,21 +33,23 @@ const communityDbUser: dbUser = { hasId: function (): boolean { throw new Error('Function not implemented.') }, - save: function (options?: SaveOptions): Promise { + save: function (_options?: SaveOptions): Promise { throw new Error('Function not implemented.') }, - remove: function (options?: RemoveOptions): Promise { + remove: function (_options?: RemoveOptions): Promise { throw new Error('Function not implemented.') }, - softRemove: function (options?: SaveOptions): Promise { + softRemove: function (_options?: SaveOptions): Promise { throw new Error('Function not implemented.') }, - recover: function (options?: SaveOptions): Promise { + recover: function (_options?: SaveOptions): Promise { throw new Error('Function not implemented.') }, reload: function (): Promise { throw new Error('Function not implemented.') }, + foreign: false, + communityUuid: '55555555-4444-4333-2222-11111111', } const communityUser = new User(communityDbUser) diff --git a/database/entity/0073-introduce_foreign_user_in_users_table/User.ts b/database/entity/0073-introduce_foreign_user_in_users_table/User.ts new file mode 100644 index 000000000..e842d8092 --- /dev/null +++ b/database/entity/0073-introduce_foreign_user_in_users_table/User.ts @@ -0,0 +1,132 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + DeleteDateColumn, + OneToMany, + JoinColumn, + OneToOne, +} from 'typeorm' +import { Contribution } from '../Contribution' +import { ContributionMessage } from '../ContributionMessage' +import { UserContact } from '../UserContact' +import { UserRole } from '../UserRole' + +@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) +export class User extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ type: 'bool', default: false }) + foreign: boolean + + @Column({ + name: 'gradido_id', + length: 36, + nullable: false, + collation: 'utf8mb4_unicode_ci', + }) + gradidoID: string + + @Column({ + name: 'community_uuid', + type: 'char', + length: 36, + nullable: true, + collation: 'utf8mb4_unicode_ci', + }) + communityUuid: string + + @Column({ + name: 'alias', + length: 20, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + alias: string + + @OneToOne(() => UserContact, (emailContact: UserContact) => emailContact.user) + @JoinColumn({ name: 'email_id' }) + emailContact: UserContact + + @Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null }) + emailId: number | null + + @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 + + @Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP(3)', nullable: false }) + createdAt: Date + + @DeleteDateColumn({ name: 'deleted_at', nullable: true }) + deletedAt: Date | null + + @Column({ type: 'bigint', default: 0, unsigned: true }) + password: BigInt + + @Column({ + name: 'password_encryption_type', + type: 'int', + unsigned: true, + nullable: false, + default: 0, + }) + passwordEncryptionType: number + + @Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false }) + language: string + + @Column({ type: 'bool', default: false }) + hideAmountGDD: boolean + + @Column({ type: 'bool', default: false }) + hideAmountGDT: boolean + + @OneToMany(() => UserRole, (userRole) => userRole.user) + @JoinColumn({ name: 'user_id' }) + userRoles: UserRole[] + + @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 + + @OneToMany(() => Contribution, (contribution) => contribution.user) + @JoinColumn({ name: 'user_id' }) + contributions?: Contribution[] + + @OneToMany(() => ContributionMessage, (message) => message.user) + @JoinColumn({ name: 'user_id' }) + messages?: ContributionMessage[] + + @OneToMany(() => UserContact, (userContact: UserContact) => userContact.user) + @JoinColumn({ name: 'user_id' }) + userContacts?: UserContact[] +} diff --git a/database/entity/User.ts b/database/entity/User.ts index eb66bdee9..21785ee9c 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0069-add_user_roles_table/User' +export { User } from './0073-introduce_foreign_user_in_users_table/User' diff --git a/database/migrations/0073-introduce_foreign_user_in_users_table.ts b/database/migrations/0073-introduce_foreign_user_in_users_table.ts new file mode 100644 index 000000000..96bc07335 --- /dev/null +++ b/database/migrations/0073-introduce_foreign_user_in_users_table.ts @@ -0,0 +1,41 @@ +/* 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` DROP KEY IF EXISTS `gradido_id`;') + await queryFn('ALTER TABLE `users` DROP INDEX IF EXISTS `gradido_id`;') + await queryFn('ALTER TABLE `users` DROP KEY IF EXISTS `alias`;') + await queryFn('ALTER TABLE `users` DROP INDEX IF EXISTS `alias`;') + await queryFn( + 'ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `foreign` tinyint(4) NOT NULL DEFAULT 0 AFTER `id`;', + ) + + await queryFn( + 'ALTER TABLE `users` ADD COLUMN IF NOT EXISTS `community_uuid` char(36) DEFAULT NULL NULL AFTER `gradido_id`;', + ) + await queryFn( + 'ALTER TABLE `users` ADD CONSTRAINT uuid_key UNIQUE KEY (`gradido_id`, `community_uuid`);', + ) + await queryFn( + 'ALTER TABLE `users` ADD CONSTRAINT alias_key UNIQUE KEY (`alias`, `community_uuid`);', + ) + // read the community uuid of the homeCommunity + const result = await queryFn(`SELECT c.community_uuid from communities as c WHERE c.foreign = 0`) + // and if uuid exists enter the home_community_uuid for all local users + if (result && result[0]) { + await queryFn( + `UPDATE users as u SET u.community_uuid = "${result[0].community_uuid}" WHERE u.foreign = 0 AND u.community_uuid IS NULL`, + ) + } +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('ALTER TABLE `users` DROP KEY IF EXISTS `uuid_key`;') + await queryFn('ALTER TABLE `users` DROP KEY IF EXISTS `alias_key`;') + + await queryFn('ALTER TABLE `users` DROP COLUMN IF EXISTS `foreign`;') + await queryFn('ALTER TABLE `users` DROP COLUMN IF EXISTS `community_uuid`;') + + await queryFn('ALTER TABLE `users` ADD CONSTRAINT gradido_id UNIQUE KEY (`gradido_id`);') + await queryFn('ALTER TABLE `users` ADD CONSTRAINT alias UNIQUE KEY (`alias`);') +} diff --git a/dht-node/src/config/index.ts b/dht-node/src/config/index.ts index 78755f280..2d88e0f92 100644 --- a/dht-node/src/config/index.ts +++ b/dht-node/src/config/index.ts @@ -4,7 +4,7 @@ import dotenv from 'dotenv' dotenv.config() const constants = { - DB_VERSION: '0072-add_communityuuid_to_transactions_table', + DB_VERSION: '0073-introduce_foreign_user_in_users_table', LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info LOG_LEVEL: process.env.LOG_LEVEL || 'info', diff --git a/e2e-tests/cypress/e2e/SendCoins.feature b/e2e-tests/cypress/e2e/SendCoins.feature.save similarity index 100% rename from e2e-tests/cypress/e2e/SendCoins.feature rename to e2e-tests/cypress/e2e/SendCoins.feature.save diff --git a/federation/src/config/index.ts b/federation/src/config/index.ts index e88267589..7cc9ef37e 100644 --- a/federation/src/config/index.ts +++ b/federation/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0072-add_communityuuid_to_transactions_table', + DB_VERSION: '0073-introduce_foreign_user_in_users_table', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info diff --git a/federation/src/graphql/api/1_0/model/SendCoinsArgs.ts b/federation/src/graphql/api/1_0/model/SendCoinsArgs.ts index e658209ca..e15f40ed3 100644 --- a/federation/src/graphql/api/1_0/model/SendCoinsArgs.ts +++ b/federation/src/graphql/api/1_0/model/SendCoinsArgs.ts @@ -26,4 +26,7 @@ export class SendCoinsArgs { @Field(() => String) senderUserName: string + + @Field(() => String, { nullable: true }) + senderAlias?: string | null } diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts index 412786fa6..d305e1726 100644 --- a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.test.ts @@ -13,7 +13,7 @@ import { Connection } from '@dbTools/typeorm' import Decimal from 'decimal.js-light' import { SendCoinsArgs } from '../model/SendCoinsArgs' -let mutate: ApolloServerTestClient['mutate'], con: Connection +let mutate: ApolloServerTestClient['mutate'] // , con: Connection // let query: ApolloServerTestClient['query'] let testEnv: { @@ -35,13 +35,15 @@ beforeAll(async () => { testEnv = await testEnvironment(logger) mutate = testEnv.mutate // query = testEnv.query - con = testEnv.con + // con = testEnv.con await cleanDB() }) afterAll(async () => { // await cleanDB() - await con.destroy() + if (!testEnv.con || !testEnv.con.isConnected) { + await testEnv.con.close() + } }) describe('SendCoinsResolver', () => { @@ -92,6 +94,7 @@ describe('SendCoinsResolver', () => { sendUser = DbUser.create() sendUser.alias = 'sendUser-alias' + sendUser.communityUuid = '56a55482-909e-46a4-bfa2-cd025e894eba' sendUser.firstName = 'sendUser-FirstName' sendUser.gradidoID = '56a55482-909e-46a4-bfa2-cd025e894ebc' sendUser.lastName = 'sendUser-LastName' @@ -106,6 +109,7 @@ describe('SendCoinsResolver', () => { recipUser = DbUser.create() recipUser.alias = 'recipUser-alias' + recipUser.communityUuid = '56a55482-909e-46a4-bfa2-cd025e894ebb' recipUser.firstName = 'recipUser-FirstName' recipUser.gradidoID = '56a55482-909e-46a4-bfa2-cd025e894ebd' recipUser.lastName = 'recipUser-LastName' @@ -134,6 +138,7 @@ describe('SendCoinsResolver', () => { } args.senderUserUuid = sendUser.gradidoID args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + args.senderAlias = sendUser.alias expect( await mutate({ mutation: voteForSendCoinsMutation, @@ -163,6 +168,7 @@ describe('SendCoinsResolver', () => { } args.senderUserUuid = sendUser.gradidoID args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + args.senderAlias = sendUser.alias expect( await mutate({ mutation: voteForSendCoinsMutation, @@ -180,7 +186,7 @@ describe('SendCoinsResolver', () => { }) }) - describe('valid X-Com-TX voted', () => { + describe('valid X-Com-TX voted per gradidoID', () => { it('throws an error', async () => { jest.clearAllMocks() const args = new SendCoinsArgs() @@ -196,6 +202,83 @@ describe('SendCoinsResolver', () => { } args.senderUserUuid = sendUser.gradidoID args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + args.senderAlias = sendUser.alias + expect( + await mutate({ + mutation: voteForSendCoinsMutation, + variables: { args }, + }), + ).toEqual( + expect.objectContaining({ + data: { + voteForSendCoins: { + recipGradidoID: '56a55482-909e-46a4-bfa2-cd025e894ebd', + recipFirstName: 'recipUser-FirstName', + recipLastName: 'recipUser-LastName', + recipAlias: 'recipUser-alias', + vote: true, + }, + }, + }), + ) + }) + }) + + describe('valid X-Com-TX voted per alias', () => { + it('throws an error', async () => { + jest.clearAllMocks() + const args = new SendCoinsArgs() + if (foreignCom.communityUuid) { + args.recipientCommunityUuid = foreignCom.communityUuid + } + args.recipientUserIdentifier = recipUser.alias + args.creationDate = new Date().toISOString() + args.amount = new Decimal(100) + args.memo = 'X-Com-TX memo' + if (homeCom.communityUuid) { + args.senderCommunityUuid = homeCom.communityUuid + } + args.senderUserUuid = sendUser.gradidoID + args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + args.senderAlias = sendUser.alias + expect( + await mutate({ + mutation: voteForSendCoinsMutation, + variables: { args }, + }), + ).toEqual( + expect.objectContaining({ + data: { + voteForSendCoins: { + recipGradidoID: '56a55482-909e-46a4-bfa2-cd025e894ebd', + recipFirstName: 'recipUser-FirstName', + recipLastName: 'recipUser-LastName', + recipAlias: 'recipUser-alias', + vote: true, + }, + }, + }), + ) + }) + }) + + describe('valid X-Com-TX voted per email', () => { + it('throws an error', async () => { + jest.clearAllMocks() + const args = new SendCoinsArgs() + if (foreignCom.communityUuid) { + args.recipientCommunityUuid = foreignCom.communityUuid + } + args.recipientUserIdentifier = recipContact.email + args.creationDate = new Date().toISOString() + args.amount = new Decimal(100) + args.memo = 'X-Com-TX memo' + if (homeCom.communityUuid) { + args.senderCommunityUuid = homeCom.communityUuid + } + args.senderUserUuid = sendUser.gradidoID + args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + args.senderAlias = sendUser.alias expect( await mutate({ mutation: voteForSendCoinsMutation, @@ -235,6 +318,7 @@ describe('SendCoinsResolver', () => { } args.senderUserUuid = sendUser.gradidoID args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + args.senderAlias = sendUser.alias await mutate({ mutation: voteForSendCoinsMutation, variables: { args }, @@ -255,6 +339,7 @@ describe('SendCoinsResolver', () => { } args.senderUserUuid = sendUser.gradidoID args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + args.senderAlias = sendUser.alias expect( await mutate({ mutation: revertSendCoinsMutation, @@ -284,6 +369,7 @@ describe('SendCoinsResolver', () => { } args.senderUserUuid = sendUser.gradidoID args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + args.senderAlias = sendUser.alias expect( await mutate({ mutation: revertSendCoinsMutation, @@ -317,6 +403,7 @@ describe('SendCoinsResolver', () => { } args.senderUserUuid = sendUser.gradidoID args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + args.senderAlias = sendUser.alias expect( await mutate({ mutation: revertSendCoinsMutation, @@ -350,6 +437,7 @@ describe('SendCoinsResolver', () => { } args.senderUserUuid = sendUser.gradidoID args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + args.senderAlias = sendUser.alias await mutate({ mutation: voteForSendCoinsMutation, variables: { args }, @@ -370,6 +458,7 @@ describe('SendCoinsResolver', () => { } args.senderUserUuid = sendUser.gradidoID args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + args.senderAlias = sendUser.alias expect( await mutate({ mutation: settleSendCoinsMutation, @@ -399,6 +488,7 @@ describe('SendCoinsResolver', () => { } args.senderUserUuid = sendUser.gradidoID args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + args.senderAlias = sendUser.alias expect( await mutate({ mutation: settleSendCoinsMutation, @@ -432,6 +522,7 @@ describe('SendCoinsResolver', () => { } args.senderUserUuid = sendUser.gradidoID args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + args.senderAlias = sendUser.alias expect( await mutate({ mutation: settleSendCoinsMutation, @@ -465,6 +556,7 @@ describe('SendCoinsResolver', () => { } args.senderUserUuid = sendUser.gradidoID args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + args.senderAlias = sendUser.alias await mutate({ mutation: voteForSendCoinsMutation, variables: { args }, @@ -489,6 +581,7 @@ describe('SendCoinsResolver', () => { } args.senderUserUuid = sendUser.gradidoID args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + args.senderAlias = sendUser.alias expect( await mutate({ mutation: revertSettledSendCoinsMutation, @@ -518,6 +611,7 @@ describe('SendCoinsResolver', () => { } args.senderUserUuid = sendUser.gradidoID args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + args.senderAlias = sendUser.alias expect( await mutate({ mutation: revertSettledSendCoinsMutation, @@ -551,6 +645,7 @@ describe('SendCoinsResolver', () => { } args.senderUserUuid = sendUser.gradidoID args.senderUserName = fullName(sendUser.firstName, sendUser.lastName) + args.senderAlias = sendUser.alias expect( await mutate({ mutation: revertSettledSendCoinsMutation, @@ -573,7 +668,7 @@ async function newEmailContact(email: string, userId: number): Promise { + if (args.senderCommunityUuid !== null && args.senderUserUuid !== null) { + try { + const user = await DbUser.findOne({ + where: { + foreign: true, + communityUuid: args.senderCommunityUuid, + gradidoID: args.senderUserUuid, + }, + }) + if (!user) { + logger.debug( + 'X-Com: no foreignUser found for:', + args.senderCommunityUuid, + args.senderUserUuid, + ) + let foreignUser = DbUser.create() + foreignUser.foreign = true + if (args.senderAlias) { + foreignUser.alias = args.senderAlias + } + foreignUser.communityUuid = args.senderCommunityUuid + if (args.senderUserName !== null) { + foreignUser.firstName = args.senderUserName.slice(0, args.senderUserName.indexOf(' ')) + foreignUser.lastName = args.senderUserName.slice( + args.senderUserName.indexOf(' '), + args.senderUserName.length, + ) + } + foreignUser.gradidoID = args.senderUserUuid + foreignUser = await DbUser.save(foreignUser) + logger.debug('X-Com: new foreignUser inserted:', foreignUser) + + return true + } else if ( + user.firstName !== args.senderUserName.slice(0, args.senderUserName.indexOf(' ')) || + user.lastName !== + args.senderUserName.slice(args.senderUserName.indexOf(' '), args.senderUserName.length) || + user.alias !== args.senderAlias + ) { + logger.warn( + 'X-Com: foreignUser still exists, but with different name or alias:', + user, + args, + ) + return false + } else { + logger.debug('X-Com: foreignUser still exists...:', user) + return true + } + } catch (err) { + logger.error('X-Com: error in storeForeignUser;', err) + return false + } + } + return false +} diff --git a/federation/src/graphql/util/findUserByIdentifier.ts b/federation/src/graphql/util/findUserByIdentifier.ts index 96c9eb458..6b7f1bccc 100644 --- a/federation/src/graphql/util/findUserByIdentifier.ts +++ b/federation/src/graphql/util/findUserByIdentifier.ts @@ -6,12 +6,18 @@ import { LogError } from '@/server/LogError' import { VALID_ALIAS_REGEX } from './validateAlias' -export const findUserByIdentifier = async (identifier: string): Promise => { +export const findUserByIdentifier = async ( + identifier: string, + communityIdentifier?: string, +): Promise => { let user: DbUser | null if (validate(identifier) && version(identifier) === 4) { - user = await DbUser.findOne({ where: { gradidoID: identifier }, relations: ['emailContact'] }) + user = await DbUser.findOne({ + where: { gradidoID: identifier, communityUuid: communityIdentifier }, + relations: ['emailContact'], + }) if (!user) { - throw new LogError('No user found to given identifier', identifier) + throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier) } } else if (/^.{2,}@.{2,}\..{2,}$/.exec(identifier)) { const userContact = await DbUserContact.findOne({ @@ -27,12 +33,22 @@ export const findUserByIdentifier = async (identifier: string): Promise if (!userContact.user) { throw new LogError('No user to given contact', identifier) } + if (userContact.user.communityUuid !== communityIdentifier) { + throw new LogError( + 'Found user to given contact, but belongs to other community', + identifier, + communityIdentifier, + ) + } user = userContact.user user.emailContact = userContact } else if (VALID_ALIAS_REGEX.exec(identifier)) { - user = await DbUser.findOne({ where: { alias: identifier }, relations: ['emailContact'] }) + user = await DbUser.findOne({ + where: { alias: identifier, communityUuid: communityIdentifier }, + relations: ['emailContact'], + }) if (!user) { - throw new LogError('No user found to given identifier', identifier) + throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier) } } else { throw new LogError('Unknown identifier type', identifier) diff --git a/frontend/src/components/GddSend/TransactionForm.vue b/frontend/src/components/GddSend/TransactionForm.vue index 296474e44..a7f22bbbc 100644 --- a/frontend/src/components/GddSend/TransactionForm.vue +++ b/frontend/src/components/GddSend/TransactionForm.vue @@ -236,6 +236,7 @@ export default { update({ user, community }) { this.userName = `${user.firstName} ${user.lastName}` this.recipientCommunity.name = community.name + this.recipientCommunity.uuid = this.communityUuid }, error({ message }) { this.toastError(message) diff --git a/frontend/src/components/TransactionRows/Name.vue b/frontend/src/components/TransactionRows/Name.vue index b4ddfc849..2f495756c 100644 --- a/frontend/src/components/TransactionRows/Name.vue +++ b/frontend/src/components/TransactionRows/Name.vue @@ -36,13 +36,24 @@ export default { methods: { async tunnelEmail() { if (this.$route.path !== '/send') await this.$router.push({ path: '/send' }) - this.$router.push({ query: { gradidoID: this.linkedUser.gradidoID } }) + this.$router.push({ + query: { + gradidoID: this.linkedUser.gradidoID, + communityUuid: this.linkedUser.communityUuid, + }, + }) }, }, computed: { itemText() { return this.linkedUser - ? this.linkedUser.firstName + ' ' + this.linkedUser.lastName + ? this.linkedUser.alias + ? this.linkedUser.alias + + (this.linkedUser.communityName ? ' / ' + this.linkedUser.communityName : '') + : this.linkedUser.firstName + + ' ' + + this.linkedUser.lastName + + (this.linkedUser.communityName ? ' / ' + this.linkedUser.communityName : '') : this.text }, }, diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 5edbc1d1f..3cdf55c3f 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -40,7 +40,10 @@ export const transactionsQuery = gql` linkedUser { firstName lastName + communityUuid + communityName gradidoID + alias } decay { decay @@ -281,6 +284,7 @@ export const user = gql` user(identifier: $identifier) { firstName lastName + communityName } } `