Merge pull request #3215 from gradido/3213-feature-x-com-sendcoins-31-insert-recipient-as-foreign-user-in-users-table-after-x-com-sendcoins

feat(backend): x-com-sendcoins 31: insert recipient as foreign user in users table after x com sendcoins
This commit is contained in:
clauspeterhuebner 2023-10-18 21:37:31 +02:00 committed by GitHub
commit 53b2fe33a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 724 additions and 66 deletions

View File

@ -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

View File

@ -26,4 +26,7 @@ export class SendCoinsArgs {
@Field(() => String)
senderUserName: string
@Field(() => String, { nullable: true })
senderAlias?: string | null
}

View File

@ -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()

View File

@ -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
}

View File

@ -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

View File

@ -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_',

View File

@ -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(

View File

@ -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,
}),

View File

@ -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<User> {
return new User(await findUserByIdentifier(identifier))
async user(
@Args()
{ identifier, communityIdentifier }: UserArgs,
): Promise<User> {
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
}
}

View File

@ -15,6 +15,12 @@ export async function isHomeCommunity(communityIdentifier: string): Promise<bool
}
}
export async function getHomeCommunity(): Promise<DbCommunity> {
return await DbCommunity.findOneOrFail({
where: [{ foreign: false }],
})
}
export async function getCommunityUrl(communityIdentifier: string): Promise<string> {
const community = await DbCommunity.findOneOrFail({
where: [

View File

@ -6,12 +6,18 @@ import { LogError } from '@/server/LogError'
import { VALID_ALIAS_REGEX } from './validateAlias'
export const findUserByIdentifier = async (identifier: string): Promise<DbUser> => {
export const findUserByIdentifier = async (
identifier: string,
communityIdentifier?: string,
): Promise<DbUser> => {
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<DbUser>
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)

View File

@ -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<SendCoinsResult> {
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)

View File

@ -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<boolean> {
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
}

View File

@ -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({

View File

@ -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
}
}
`

View File

@ -33,21 +33,23 @@ const communityDbUser: dbUser = {
hasId: function (): boolean {
throw new Error('Function not implemented.')
},
save: function (options?: SaveOptions): Promise<dbUser> {
save: function (_options?: SaveOptions): Promise<dbUser> {
throw new Error('Function not implemented.')
},
remove: function (options?: RemoveOptions): Promise<dbUser> {
remove: function (_options?: RemoveOptions): Promise<dbUser> {
throw new Error('Function not implemented.')
},
softRemove: function (options?: SaveOptions): Promise<dbUser> {
softRemove: function (_options?: SaveOptions): Promise<dbUser> {
throw new Error('Function not implemented.')
},
recover: function (options?: SaveOptions): Promise<dbUser> {
recover: function (_options?: SaveOptions): Promise<dbUser> {
throw new Error('Function not implemented.')
},
reload: function (): Promise<void> {
throw new Error('Function not implemented.')
},
foreign: false,
communityUuid: '55555555-4444-4333-2222-11111111',
}
const communityUser = new User(communityDbUser)

View File

@ -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[]
}

View File

@ -1 +1 @@
export { User } from './0069-add_user_roles_table/User'
export { User } from './0073-introduce_foreign_user_in_users_table/User'

View File

@ -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<Array<any>>) {
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<Array<any>>) {
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`);')
}

View File

@ -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',

View File

@ -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

View File

@ -26,4 +26,7 @@ export class SendCoinsArgs {
@Field(() => String)
senderUserName: string
@Field(() => String, { nullable: true })
senderAlias?: string | null
}

View File

@ -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<DbUserCon
emailContact.email = email
emailContact.userId = userId
emailContact.type = 'EMAIL'
emailContact.emailChecked = false
emailContact.emailChecked = true
emailContact.emailOptInTypeId = 1
emailContact.emailVerificationCode = '1' + userId
return emailContact

View File

@ -15,6 +15,7 @@ import { revertSettledReceiveTransaction } from '../util/revertSettledReceiveTra
import { findUserByIdentifier } from '@/graphql/util/findUserByIdentifier'
import { SendCoinsResult } from '../model/SendCoinsResult'
import Decimal from 'decimal.js-light'
import { storeForeignUser } from '../util/storeForeignUser'
@Resolver()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -34,6 +35,7 @@ export class SendCoinsResolver {
args.senderCommunityUuid,
args.senderUserUuid,
args.senderUserName,
args.senderAlias,
)
const result = new SendCoinsResult()
// first check if receiver community is correct
@ -49,7 +51,10 @@ export class SendCoinsResolver {
let receiverUser
try {
// second check if receiver user exists in this community
receiverUser = await findUserByIdentifier(args.recipientUserIdentifier)
receiverUser = await findUserByIdentifier(
args.recipientUserIdentifier,
args.recipientCommunityUuid,
)
} catch (err) {
logger.error('Error in findUserByIdentifier:', err)
throw new LogError(
@ -236,6 +241,16 @@ export class SendCoinsResolver {
logger.debug('XCom: settleSendCoins matching pendingTX for settlement...')
await settlePendingReceiveTransaction(homeCom, receiverUser, pendingTx)
// after successful x-com-tx store the recipient as foreign user
logger.debug('store recipient as foreign user...')
if (await storeForeignUser(args)) {
logger.info(
'X-Com: new foreign user inserted successfully...',
args.senderCommunityUuid,
args.senderUserUuid,
)
}
logger.debug(`XCom: settlePendingReceiveTransaction()-1_0... successfull`)
return true
} else {

View File

@ -0,0 +1,62 @@
import { User as DbUser } from '@entity/User'
import { federationLogger as logger } from '@/server/logger'
import { SendCoinsArgs } from '../model/SendCoinsArgs'
export async function storeForeignUser(args: SendCoinsArgs): Promise<boolean> {
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
}

View File

@ -6,12 +6,18 @@ import { LogError } from '@/server/LogError'
import { VALID_ALIAS_REGEX } from './validateAlias'
export const findUserByIdentifier = async (identifier: string): Promise<DbUser> => {
export const findUserByIdentifier = async (
identifier: string,
communityIdentifier?: string,
): Promise<DbUser> => {
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<DbUser>
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)

View File

@ -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)

View File

@ -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
},
},

View File

@ -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
}
}
`