Merge branch 'master' into feat-previous-balance

This commit is contained in:
Moriz Wahl 2023-04-04 16:55:22 +02:00
commit c3ebc33ae5
47 changed files with 520 additions and 304 deletions

View File

@ -5,7 +5,7 @@ module.exports = {
node: true,
},
parser: '@typescript-eslint/parser',
plugins: ['prettier', '@typescript-eslint', 'type-graphql', 'jest', 'import'],
plugins: ['prettier', '@typescript-eslint', 'type-graphql', 'jest', 'import', 'n'],
extends: [
'standard',
'eslint:recommended',
@ -101,6 +101,45 @@ module.exports = {
},
],
'import/prefer-default-export': 'off', // TODO
// n
'n/handle-callback-err': 'error',
'n/no-callback-literal': 'error',
'n/no-exports-assign': 'error',
'n/no-extraneous-import': 'error',
'n/no-extraneous-require': 'error',
'n/no-hide-core-modules': 'error',
'n/no-missing-import': 'off', // not compatible with typescript
'n/no-missing-require': 'error',
'n/no-new-require': 'error',
'n/no-path-concat': 'error',
'n/no-process-exit': 'error',
'n/no-unpublished-bin': 'error',
'n/no-unpublished-import': 'off', // TODO need to exclude seeds
'n/no-unpublished-require': 'error',
'n/no-unsupported-features': ['error', { ignores: ['modules'] }],
'n/no-unsupported-features/es-builtins': 'error',
'n/no-unsupported-features/es-syntax': 'error',
'n/no-unsupported-features/node-builtins': 'error',
'n/process-exit-as-throw': 'error',
'n/shebang': 'error',
'n/callback-return': 'error',
'n/exports-style': 'error',
'n/file-extension-in-import': 'off',
'n/global-require': 'error',
'n/no-mixed-requires': 'error',
'n/no-process-env': 'error',
'n/no-restricted-import': 'error',
'n/no-restricted-require': 'error',
'n/no-sync': 'error',
'n/prefer-global/buffer': 'error',
'n/prefer-global/console': 'error',
'n/prefer-global/process': 'error',
'n/prefer-global/text-decoder': 'error',
'n/prefer-global/text-encoder': 'error',
'n/prefer-global/url': 'error',
'n/prefer-global/url-search-params': 'error',
'n/prefer-promises/dns': 'error',
'n/prefer-promises/fs': 'error',
},
overrides: [
// only for ts files

View File

@ -22,10 +22,12 @@ module.exports = {
'@repository/(.*)': '<rootDir>/src/typeorm/repository/$1',
'@test/(.*)': '<rootDir>/test/$1',
'@entity/(.*)':
// eslint-disable-next-line n/no-process-env
process.env.NODE_ENV === 'development'
? '<rootDir>/../database/entity/$1'
: '<rootDir>/../database/build/entity/$1',
'@dbTools/(.*)':
// eslint-disable-next-line n/no-process-env
process.env.NODE_ENV === 'development'
? '<rootDir>/../database/src/$1'
: '<rootDir>/../database/build/src/$1',

View File

@ -65,6 +65,7 @@
"eslint-import-resolver-typescript": "^3.5.3",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jest": "^27.2.1",
"eslint-plugin-n": "^15.6.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-promise": "^5.1.0",
@ -84,5 +85,8 @@
"ignore": [
"**/*.test.ts"
]
},
"engines": {
"node": ">=14"
}
}

View File

@ -35,6 +35,7 @@ export enum RIGHTS {
CREATE_CONTRIBUTION_MESSAGE = 'CREATE_CONTRIBUTION_MESSAGE',
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
OPEN_CREATIONS = 'OPEN_CREATIONS',
USER = 'USER',
// Admin
SEARCH_USERS = 'SEARCH_USERS',
SET_USER_ROLE = 'SET_USER_ROLE',

View File

@ -34,6 +34,7 @@ export const ROLE_USER = new Role('user', [
RIGHTS.CREATE_CONTRIBUTION_MESSAGE,
RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES,
RIGHTS.OPEN_CREATIONS,
RIGHTS.USER,
])
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights

View File

@ -1,4 +1,5 @@
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
/* eslint-disable n/no-process-env */
import { Decimal } from 'decimal.js-light'
import dotenv from 'dotenv'

View File

@ -4,7 +4,7 @@ import { ArgsType, Field } from 'type-graphql'
@ArgsType()
export default class TransactionSendArgs {
@Field(() => String)
email: string
identifier: string
@Field(() => Decimal)
amount: Decimal

View File

@ -27,8 +27,6 @@ import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
import { peterLustig } from '@/seeds/users/peter-lustig'
import { stephenHawking } from '@/seeds/users/stephen-hawking'
import { findUserByEmail } from './UserResolver'
let mutate: any, query: any, con: any
let testEnv: any
@ -84,7 +82,7 @@ describe('send coins', () => {
await mutate({
mutation: sendCoins,
variables: {
email: 'wrong@email.com',
identifier: 'wrong@email.com',
amount: 100,
memo: 'test',
},
@ -112,22 +110,20 @@ describe('send coins', () => {
await mutate({
mutation: sendCoins,
variables: {
email: 'stephen@hawking.uk',
identifier: 'stephen@hawking.uk',
amount: 100,
memo: 'test',
},
}),
).toEqual(
expect.objectContaining({
errors: [new GraphQLError('The recipient account was deleted')],
errors: [new GraphQLError('No user to given contact')],
}),
)
})
it('logs the error thrown', async () => {
// find peter to check the log
const user = await findUserByEmail('stephen@hawking.uk')
expect(logger.error).toBeCalledWith('The recipient account was deleted', user)
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('No user to given contact', 'stephen@hawking.uk')
})
})
@ -143,22 +139,23 @@ describe('send coins', () => {
await mutate({
mutation: sendCoins,
variables: {
email: 'garrick@ollivander.com',
identifier: 'garrick@ollivander.com',
amount: 100,
memo: 'test',
},
}),
).toEqual(
expect.objectContaining({
errors: [new GraphQLError('The recipient account is not activated')],
errors: [new GraphQLError('No user with this credentials')],
}),
)
})
it('logs the error thrown', async () => {
// find peter to check the log
const user = await findUserByEmail('garrick@ollivander.com')
expect(logger.error).toBeCalledWith('The recipient account is not activated', user)
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'No user with this credentials',
'garrick@ollivander.com',
)
})
})
})
@ -178,7 +175,7 @@ describe('send coins', () => {
await mutate({
mutation: sendCoins,
variables: {
email: 'bob@baumeister.de',
identifier: 'bob@baumeister.de',
amount: 100,
memo: 'test',
},
@ -202,7 +199,7 @@ describe('send coins', () => {
await mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
identifier: 'peter@lustig.de',
amount: 100,
memo: 'test',
},
@ -226,7 +223,7 @@ describe('send coins', () => {
await mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
identifier: 'peter@lustig.de',
amount: 100,
memo: 'test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test t',
},
@ -250,7 +247,7 @@ describe('send coins', () => {
await mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
identifier: 'peter@lustig.de',
amount: 100,
memo: 'testing',
},
@ -300,7 +297,7 @@ describe('send coins', () => {
await mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
identifier: 'peter@lustig.de',
amount: -50,
memo: 'testing negative',
},
@ -323,7 +320,7 @@ describe('send coins', () => {
await mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
identifier: 'peter@lustig.de',
amount: 50,
memo: 'unrepeatable memo',
},
@ -380,7 +377,7 @@ describe('send coins', () => {
mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
identifier: 'peter@lustig.de',
amount: 10,
memo: 'first transaction',
},
@ -396,7 +393,7 @@ describe('send coins', () => {
mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
identifier: 'peter@lustig.de',
amount: 20,
memo: 'second transaction',
},
@ -412,7 +409,7 @@ describe('send coins', () => {
mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
identifier: 'peter@lustig.de',
amount: 30,
memo: 'third transaction',
},
@ -428,7 +425,7 @@ describe('send coins', () => {
mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
identifier: 'peter@lustig.de',
amount: 40,
memo: 'fourth transaction',
},

View File

@ -36,7 +36,7 @@ import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualT
import { BalanceResolver } from './BalanceResolver'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
import { findUserByEmail } from './UserResolver'
import { findUserByIdentifier } from './util/findUserByIdentifier'
import { getLastTransaction } from './util/getLastTransaction'
export const executeTransaction = async (
@ -150,7 +150,6 @@ export const executeTransaction = async (
} finally {
await queryRunner.release()
}
logger.debug(`prepare Email for transaction received...`)
await sendTransactionReceivedEmail({
firstName: recipient.firstName,
lastName: recipient.lastName,
@ -302,10 +301,10 @@ export class TransactionResolver {
@Authorized([RIGHTS.SEND_COINS])
@Mutation(() => Boolean)
async sendCoins(
@Args() { email, amount, memo }: TransactionSendArgs,
@Args() { identifier, amount, memo }: TransactionSendArgs,
@Ctx() context: Context,
): Promise<boolean> {
logger.info(`sendCoins(email=${email}, amount=${amount}, memo=${memo})`)
logger.info(`sendCoins(identifier=${identifier}, amount=${amount}, memo=${memo})`)
if (amount.lte(0)) {
throw new LogError('Amount to send must be positive', amount)
}
@ -314,13 +313,9 @@ export class TransactionResolver {
const senderUser = getUser(context)
// validate recipient user
const recipientUser = await findUserByEmail(email)
if (recipientUser.deletedAt) {
throw new LogError('The recipient account was deleted', recipientUser)
}
const emailContact = recipientUser.emailContact
if (!emailContact.emailChecked) {
throw new LogError('The recipient account is not activated', recipientUser)
const recipientUser = await findUserByIdentifier(identifier)
if (!recipientUser) {
throw new LogError('The recipient user was not found', recipientUser)
}
await executeTransaction(amount, memo, senderUser, recipientUser)

View File

@ -11,7 +11,7 @@ import { TransactionLink } from '@entity/TransactionLink'
import { User } from '@entity/User'
import { UserContact } from '@entity/UserContact'
import { GraphQLError } from 'graphql'
import { validate as validateUUID, version as versionUUID } from 'uuid'
import { v4 as uuidv4, validate as validateUUID, version as versionUUID } from 'uuid'
import { OptInType } from '@enum/OptInType'
import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
@ -46,7 +46,13 @@ import {
unDeleteUser,
sendActivationEmail,
} from '@/seeds/graphql/mutations'
import { verifyLogin, queryOptIn, searchAdminUsers, searchUsers } from '@/seeds/graphql/queries'
import {
verifyLogin,
queryOptIn,
searchAdminUsers,
searchUsers,
user as userQuery,
} from '@/seeds/graphql/queries'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
@ -2298,6 +2304,124 @@ describe('UserResolver', () => {
})
})
})
describe('user', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('unauthenticated', () => {
it('throws and logs "401 Unauthorized" error', async () => {
await expect(
query({
query: userQuery,
variables: {
identifier: 'identifier',
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
expect(logger.error).toBeCalledWith('401 Unauthorized')
})
})
describe('authenticated', () => {
const uuid = uuidv4()
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
describe('identifier is no gradido ID and no email', () => {
it('throws and logs "Unknown identifier type" error', async () => {
await expect(
query({
query: userQuery,
variables: {
identifier: 'identifier',
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('Unknown identifier type')],
}),
)
expect(logger.error).toBeCalledWith('Unknown identifier type', 'identifier')
})
})
describe('identifier is not found', () => {
it('throws and logs "No user found to given identifier" error', async () => {
await expect(
query({
query: userQuery,
variables: {
identifier: uuid,
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('No user found to given identifier')],
}),
)
expect(logger.error).toBeCalledWith('No user found to given identifier', uuid)
})
})
describe('identifier is found via email', () => {
it('returns user', async () => {
await expect(
query({
query: userQuery,
variables: {
identifier: 'bibi@bloxberg.de',
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
user: {
firstName: 'Bibi',
lastName: 'Bloxberg',
},
},
errors: undefined,
}),
)
})
})
describe('identifier is found via gradidoID', () => {
it('returns user', async () => {
await expect(
query({
query: userQuery,
variables: {
identifier: user.gradidoID,
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
user: {
firstName: 'Bibi',
lastName: 'Bloxberg',
},
},
errors: undefined,
}),
)
})
})
})
})
})
describe('printTimeDuration', () => {

View File

@ -72,6 +72,7 @@ import { getTimeDurationObject, printTimeDuration } from '@/util/time'
import { FULL_CREATION_AVAILABLE } from './const/const'
import { getUserCreations } from './util/creations'
import { findUserByIdentifier } from './util/findUserByIdentifier'
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs
const random = require('random-bigint')
@ -819,6 +820,12 @@ export class UserResolver {
return true
}
@Authorized([RIGHTS.USER])
@Query(() => User)
async user(@Arg('identifier') identifier: string): Promise<User> {
return new User(await findUserByIdentifier(identifier))
}
}
export async function findUserByEmail(email: string): Promise<DbUser> {

View File

@ -152,7 +152,7 @@ describe('semaphore', () => {
})
const bibisTransaction = mutate({
mutation: sendCoins,
variables: { email: 'bob@baumeister.de', amount: '50', memo: 'Das ist für dich, Bob' },
variables: { identifier: 'bob@baumeister.de', amount: '50', memo: 'Das ist für dich, Bob' },
})
await mutate({
mutation: login,
@ -168,7 +168,7 @@ describe('semaphore', () => {
})
const bobsTransaction = mutate({
mutation: sendCoins,
variables: { email: 'bibi@bloxberg.de', amount: '50', memo: 'Das ist für dich, Bibi' },
variables: { identifier: 'bibi@bloxberg.de', amount: '50', memo: 'Das ist für dich, Bibi' },
})
await mutate({
mutation: login,

View File

@ -0,0 +1,36 @@
import { User as DbUser } from '@entity/User'
import { UserContact as DbUserContact } from '@entity/UserContact'
import { validate, version } from 'uuid'
import LogError from '@/server/LogError'
export const findUserByIdentifier = async (identifier: string): Promise<DbUser> => {
let user: DbUser | undefined
if (validate(identifier) && version(identifier) === 4) {
user = await DbUser.findOne({ where: { gradidoID: identifier }, relations: ['emailContact'] })
if (!user) {
throw new LogError('No user found to given identifier', identifier)
}
} else if (/^.{2,}@.{2,}\..{2,}$/.exec(identifier)) {
const userContact = await DbUserContact.findOne(
{
email: identifier,
emailChecked: true,
},
{ relations: ['user'] },
)
if (!userContact) {
throw new LogError('No user with this credentials', identifier)
}
if (!userContact.user) {
throw new LogError('No user to given contact', identifier)
}
user = userContact.user
user.emailContact = userContact
} else {
// last is alias when implemented
throw new LogError('Unknown identifier type', identifier)
}
return user
}

View File

@ -1,6 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// config
import CONFIG from './config'
import { startValidateCommunities } from './federation/validateCommunities'
import createServer from './server/createServer'
@ -22,5 +20,5 @@ async function main() {
main().catch((e) => {
// eslint-disable-next-line no-console
console.error(e)
process.exit(1)
throw e
})

View File

@ -28,6 +28,7 @@ export const klicktippNewsletterStateMiddleware: MiddlewareFn = async (
{ root, args, context, info },
next,
) => {
// eslint-disable-next-line n/callback-return
const result = await next()
let klickTipp = new KlickTipp({ status: 'Unsubscribed' })
if (CONFIG.KLICKTIPP) {

View File

@ -75,8 +75,8 @@ export const sendActivationEmail = gql`
`
export const sendCoins = gql`
mutation ($email: String!, $amount: Decimal!, $memo: String!) {
sendCoins(email: $email, amount: $amount, memo: $memo)
mutation ($identifier: String!, $amount: Decimal!, $memo: String!) {
sendCoins(identifier: $identifier, amount: $amount, memo: $memo)
}
`

View File

@ -340,3 +340,12 @@ export const listContributionMessages = gql`
}
}
`
export const user = gql`
query ($identifier: String!) {
user(identifier: $identifier) {
firstName
lastName
}
}
`

View File

@ -61,6 +61,7 @@ ${JSON.stringify(requestContext.response.errors, null, 2)}`)
}
const plugins =
// eslint-disable-next-line n/no-process-env
process.env.NODE_ENV === 'development' ? [setHeadersPlugin] : [setHeadersPlugin, logPlugin]
export default plugins

View File

@ -6,7 +6,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { initialize } from '@dbTools/helpers'
import { entities } from '@entity/index'
import { createTestClient } from 'apollo-server-testing'
@ -40,7 +39,6 @@ export const testEnvironment = async (testLogger: any = logger, testI18n: any =
const testClient = createTestClient(server.apollo)
const mutate = testClient.mutate
const query = testClient.query
await initialize()
return { mutate, query, con }
}

View File

@ -1998,6 +1998,13 @@ buffer@^6.0.3:
base64-js "^1.3.1"
ieee754 "^1.2.1"
builtins@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.0.1.tgz#87f6db9ab0458be728564fa81d876d8d74552fa9"
integrity sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==
dependencies:
semver "^7.0.0"
busboy@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b"
@ -2937,6 +2944,14 @@ eslint-plugin-es@^3.0.0:
eslint-utils "^2.0.0"
regexpp "^3.0.0"
eslint-plugin-es@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz#f0822f0c18a535a97c3e714e89f88586a7641ec9"
integrity sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==
dependencies:
eslint-utils "^2.0.0"
regexpp "^3.0.0"
eslint-plugin-import@^2.27.5:
version "2.27.5"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz#876a6d03f52608a3e5bb439c2550588e51dd6c65"
@ -2965,6 +2980,20 @@ eslint-plugin-jest@^27.2.1:
dependencies:
"@typescript-eslint/utils" "^5.10.0"
eslint-plugin-n@^15.6.1:
version "15.6.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-n/-/eslint-plugin-n-15.6.1.tgz#f7e77f24abb92a550115cf11e29695da122c398c"
integrity sha512-R9xw9OtCRxxaxaszTQmQAlPgM+RdGjaL1akWuY/Fv9fRAi8Wj4CUKc6iYVG8QNRjRuo8/BqVYIpfqberJUEacA==
dependencies:
builtins "^5.0.1"
eslint-plugin-es "^4.1.0"
eslint-utils "^3.0.0"
ignore "^5.1.1"
is-core-module "^2.11.0"
minimatch "^3.1.2"
resolve "^1.22.1"
semver "^7.3.8"
eslint-plugin-node@^11.1.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d"
@ -6300,7 +6329,7 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.2.0, semver@^6.3.0:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
semver@^7.3.7:
semver@^7.0.0, semver@^7.3.7, semver@^7.3.8:
version "7.3.8"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==

View File

@ -4,5 +4,3 @@ DB_USER=root
DB_PASSWORD=
DB_DATABASE=gradido_community
MIGRATIONS_TABLE=migrations
TYPEORM_SEEDING_FACTORIES=src/factories/**/*{.ts,.js}

View File

@ -6,5 +6,3 @@ DB_USER=$DB_USER
DB_PASSWORD=$DB_PASSWORD
DB_DATABASE=gradido_community
MIGRATIONS_TABLE=migrations
TYPEORM_SEEDING_FACTORIES=src/factories/**/*{.ts,.js}

View File

@ -1,15 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const CONFIG = require('./src/config')
module.export = {
name: 'default',
type: 'mysql',
host: CONFIG.DB_HOST,
port: CONFIG.DB_PORT,
username: CONFIG.DB_USER,
password: CONFIG.DB_PASSWORD,
database: CONFIG.DB_DATABASE,
seeds: ['src/seeds/**/*{.ts,.js}'],
factories: ['src/factories/**/*{.ts,.js}'],
}

View File

@ -1,34 +0,0 @@
import CONFIG from './config'
import { createPool, PoolConfig } from 'mysql'
import { Migration } from 'ts-mysql-migrate'
import path from 'path'
const poolConfig: PoolConfig = {
host: CONFIG.DB_HOST,
port: CONFIG.DB_PORT,
user: CONFIG.DB_USER,
password: CONFIG.DB_PASSWORD,
database: CONFIG.DB_DATABASE,
}
// Pool?
const pool = createPool(poolConfig)
// Create & Initialize Migrations
const migration = new Migration({
conn: pool,
tableName: CONFIG.MIGRATIONS_TABLE,
silent: true,
dir: path.join(__dirname, '..', 'migrations'),
})
const initialize = async (): Promise<void> => {
await migration.initialize()
}
const resetDB = async (closePool = false): Promise<void> => {
await migration.reset() // use for resetting database
if (closePool) pool.end()
}
export { resetDB, pool, migration, initialize }

View File

@ -1,18 +1,29 @@
import 'reflect-metadata'
import prepare from './prepare'
import connection from './typeorm/connection'
import { resetDB, pool, migration } from './helpers'
import { createDatabase } from './prepare'
import CONFIG from './config'
import { createPool } from 'mysql'
import { Migration } from 'ts-mysql-migrate'
import path from 'path'
const run = async (command: string) => {
// Database actions not supported by our migration library
await prepare()
// Database connection for TypeORM
const con = await connection()
if (!con || !con.isConnected) {
throw new Error(`Couldn't open connection to database`)
}
await createDatabase()
// Initialize Migrations
const pool = createPool({
host: CONFIG.DB_HOST,
port: CONFIG.DB_PORT,
user: CONFIG.DB_USER,
password: CONFIG.DB_PASSWORD,
database: CONFIG.DB_DATABASE,
})
const migration = new Migration({
conn: pool,
tableName: CONFIG.MIGRATIONS_TABLE,
silent: true,
dir: path.join(__dirname, '..', 'migrations'),
})
await migration.initialize()
// Execute command
@ -25,14 +36,13 @@ const run = async (command: string) => {
break
case 'reset':
// TODO protect from production
await resetDB() // use for resetting database
await migration.reset()
break
default:
throw new Error(`Unsupported command ${command}`)
}
// Terminate connections gracefully
await con.close()
pool.end()
}

View File

@ -1,12 +0,0 @@
import Decimal from 'decimal.js-light'
export interface TransactionContext {
typeId: number
userId: number
balance: Decimal
balanceDate: Date
amount: Decimal
memo: string
creationDate?: Date
sendReceiverUserId?: number
}

View File

@ -1,26 +0,0 @@
export interface UserContext {
pubKey?: Buffer
email?: string
firstName?: string
lastName?: string
deletedAt?: Date
password?: BigInt
privKey?: Buffer
emailHash?: Buffer
createdAt?: Date
emailChecked?: boolean
language?: string
publisherId?: number
passphrase?: string
}
export interface ServerUserContext {
username?: string
password?: string
email?: string
role?: string
activated?: number
lastLogin?: Date
created?: Date
modified?: Date
}

View File

@ -1,32 +0,0 @@
import Decimal from 'decimal.js-light'
export interface UserInterface {
// from user
email?: string
firstName?: string
lastName?: string
password?: BigInt
pubKey?: Buffer
privKey?: Buffer
emailHash?: Buffer
createdAt?: Date
emailChecked?: boolean
language?: string
deletedAt?: Date
publisherId?: number
passphrase?: string
// from server user
serverUserPassword?: string
role?: string
activated?: number
lastLogin?: Date
modified?: Date
// flag for admin
isAdmin?: boolean
// flag for balance (creation of 1000 GDD)
addBalance?: boolean
// balance
recordDate?: Date
creationDate?: Date
amount?: Decimal
}

View File

@ -1,15 +1,8 @@
/* PREPARE SCRIPT
*
* This file ensures operations our migration library
* can not take care of are done.
* This applies to all Database Operations like
* creating, deleting, renaming Databases.
*/
import { createConnection } from 'mysql2/promise'
import { createConnection, RowDataPacket } from 'mysql2/promise'
import CONFIG from './config'
export default async (): Promise<void> => {
export const createDatabase = async (): Promise<void> => {
const con = await createConnection({
host: CONFIG.DB_HOST,
port: CONFIG.DB_PORT,
@ -25,6 +18,8 @@ export default async (): Promise<void> => {
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;`)
/* LEGACY CODE
import { RowDataPacket } from 'mysql2/promise'
// Check if old migration table is present, delete if needed
const [rows] = await con.query(`SHOW TABLES FROM \`${CONFIG.DB_DATABASE}\` LIKE 'migrations';`)
if ((<RowDataPacket>rows).length > 0) {
@ -37,6 +32,7 @@ export default async (): Promise<void> => {
console.log('Found and dropped old migrations table')
}
}
*/
await con.end()
}

View File

@ -1,27 +0,0 @@
import { createConnection, Connection } from 'typeorm'
import CONFIG from '../config'
import { entities } from '../../entity/index'
const connection = async (): Promise<Connection | null> => {
let con = null
try {
con = await createConnection({
name: 'default',
type: 'mysql',
host: CONFIG.DB_HOST,
port: CONFIG.DB_PORT,
username: CONFIG.DB_USER,
password: CONFIG.DB_PASSWORD,
database: CONFIG.DB_DATABASE,
entities,
synchronize: false,
})
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)
}
return con
}
export default connection

View File

@ -4,7 +4,6 @@
import CONFIG from '@/config'
import connection from '@/typeorm/connection'
import { checkDBVersion } from '@/typeorm/DBVersion'
import { initialize } from '@dbTools/helpers'
import { entities } from '@entity/index'
import { logger } from './testSetup'
@ -42,7 +41,6 @@ export const testEnvironment = async () => {
logger.fatal('Fatal: Database Version incorrect')
throw new Error('Fatal: Database Version incorrect')
}
await initialize()
return { con }
}

View File

@ -5,9 +5,7 @@
<b-row class="mt-5">
<b-col cols="2"></b-col>
<b-col>
<div class="h4">
{{ email }}
</div>
<div class="h4">{{ userName ? userName : identifier }}</div>
<div class="mt-3 h5">{{ $t('form.memo') }}</div>
<div>{{ memo }}</div>
</b-col>
@ -64,9 +62,10 @@ export default {
name: 'TransactionConfirmationSend',
props: {
balance: { type: Number, required: true },
email: { type: String, required: false, default: '' },
identifier: { type: String, required: false, default: '' },
amount: { type: Number, required: true },
memo: { type: String, required: true },
userName: { type: String, default: '' },
},
data() {
return {

View File

@ -2,9 +2,17 @@ import { mount } from '@vue/test-utils'
import TransactionForm from './TransactionForm'
import flushPromises from 'flush-promises'
import { SEND_TYPES } from '@/pages/Send'
import DashboardLayout from '@/layouts/DashboardLayout'
import { createMockClient } from 'mock-apollo-client'
import VueApollo from 'vue-apollo'
import { user as userQuery } from '@/graphql/queries'
const mockClient = createMockClient()
const apolloProvider = new VueApollo({
defaultClient: mockClient,
})
const localVue = global.localVue
localVue.use(VueApollo)
describe('TransactionForm', () => {
let wrapper
@ -22,6 +30,7 @@ describe('TransactionForm', () => {
},
$route: {
params: {},
query: {},
},
}
@ -34,10 +43,24 @@ describe('TransactionForm', () => {
localVue,
mocks,
propsData,
provide: DashboardLayout.provide,
apolloProvider,
})
}
const userQueryMock = jest.fn()
mockClient.setRequestHandler(
userQuery,
userQueryMock.mockRejectedValueOnce({ message: 'Query user name fails!' }).mockResolvedValue({
data: {
user: {
firstName: 'Bibi',
lastName: 'Bloxberg',
},
},
}),
)
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
@ -139,7 +162,7 @@ describe('TransactionForm', () => {
.setValue(' valid@email.com ')
await wrapper.find('div[data-test="input-email"]').find('input').trigger('blur')
await flushPromises()
expect(wrapper.vm.form.email).toBe('valid@email.com')
expect(wrapper.vm.form.identifier).toBe('valid@email.com')
})
})
@ -290,12 +313,12 @@ Die ganze Welt bezwingen.“`)
.find('textarea')
.setValue('Long enough')
await flushPromises()
expect(wrapper.vm.form.email).toBe('someone@watches.tv')
expect(wrapper.vm.form.identifier).toBe('someone@watches.tv')
expect(wrapper.vm.form.amount).toBe('87.23')
expect(wrapper.vm.form.memo).toBe('Long enough')
await wrapper.find('button[type="reset"]').trigger('click')
await flushPromises()
expect(wrapper.vm.form.email).toBe('')
expect(wrapper.vm.form.identifier).toBe('')
expect(wrapper.vm.form.amount).toBe('')
expect(wrapper.vm.form.memo).toBe('')
})
@ -321,10 +344,11 @@ Die ganze Welt bezwingen.“`)
expect(wrapper.emitted('set-transaction')).toEqual([
[
{
email: 'someone@watches.tv',
identifier: 'someone@watches.tv',
amount: 87.23,
memo: 'Long enough',
selected: 'send',
userName: '',
},
],
])
@ -346,5 +370,26 @@ Die ganze Welt bezwingen.“`)
})
})
})
describe('with gradido ID', () => {
beforeEach(async () => {
jest.clearAllMocks()
mocks.$route.query.gradidoID = 'gradido-ID'
wrapper = Wrapper()
await wrapper.vm.$nextTick()
})
describe('query for username with success', () => {
it('has no email input field', () => {
expect(wrapper.find('div[data-test="input-email"]').exists()).toBe(false)
})
it('queries the username', () => {
expect(userQueryMock).toBeCalledWith({
identifier: 'gradido-ID',
})
})
})
})
})
})

View File

@ -50,16 +50,24 @@
<b-col>
<b-row>
<b-col cols="12">
<div v-if="radioSelected === sendTypes.send">
<div v-if="radioSelected === sendTypes.send && !gradidoID">
<input-email
:name="$t('form.recipient')"
:label="$t('form.recipient')"
:placeholder="$t('form.email')"
v-model="form.email"
v-model="form.identifier"
:disabled="isBalanceDisabled"
@onValidation="onValidation"
/>
</div>
<div v-else-if="gradidoID" class="mb-4">
<b-row>
<b-col>{{ $t('form.recipient') }}</b-col>
</b-row>
<b-row>
<b-col class="font-weight-bold">{{ userName }}</b-col>
</b-row>
</div>
</b-col>
<b-col cols="12" lg="6">
<input-amount
@ -121,6 +129,7 @@ import { SEND_TYPES } from '@/pages/Send'
import InputEmail from '@/components/Inputs/InputEmail'
import InputAmount from '@/components/Inputs/InputAmount'
import InputTextarea from '@/components/Inputs/InputTextarea'
import { user as userQuery } from '@/graphql/queries'
export default {
name: 'TransactionForm',
@ -131,20 +140,20 @@ export default {
},
props: {
balance: { type: Number, default: 0 },
email: { type: String, default: '' },
identifier: { type: String, default: '' },
amount: { type: Number, default: 0 },
memo: { type: String, default: '' },
selected: { type: String, default: 'send' },
},
inject: ['getTunneledEmail'],
data() {
return {
form: {
email: this.email,
identifier: this.identifier,
amount: this.amount ? String(this.amount) : '',
memo: this.memo,
},
radioSelected: this.selected,
userName: '',
}
},
methods: {
@ -152,33 +161,48 @@ export default {
this.$refs.formValidator.validate()
},
onSubmit() {
if (this.gradidoID) this.form.identifier = this.gradidoID
this.$emit('set-transaction', {
selected: this.radioSelected,
email: this.form.email,
identifier: this.form.identifier,
amount: Number(this.form.amount.replace(',', '.')),
memo: this.form.memo,
userName: this.userName,
})
},
onReset(event) {
event.preventDefault()
this.form.email = ''
this.form.identifier = ''
this.form.amount = ''
this.form.memo = ''
this.$refs.formValidator.validate()
},
setNewRecipientEmail() {
this.form.email = this.recipientEmail ? this.recipientEmail : this.form.email
if (this.$route.query && !this.$route.query === {}) this.$router.replace({ query: undefined })
},
},
watch: {
recipientEmail() {
this.setNewRecipientEmail()
apollo: {
UserName: {
query() {
return userQuery
},
fetchPolicy: 'network-only',
variables() {
return { identifier: this.gradidoID }
},
skip() {
return !this.gradidoID
},
update({ user }) {
this.userName = `${user.firstName} ${user.lastName}`
},
error({ message }) {
this.toastError(message)
},
},
},
computed: {
disabled() {
if (
this.form.email.length > 5 &&
this.form.identifier.length > 5 &&
parseInt(this.form.amount) <= parseInt(this.balance) &&
this.form.memo.length > 5 &&
this.form.memo.length <= 255
@ -193,15 +217,12 @@ export default {
sendTypes() {
return SEND_TYPES
},
recipientEmail() {
return this.getTunneledEmail()
gradidoID() {
return this.$route.query && this.$route.query.gradidoID
},
},
created() {
this.setNewRecipientEmail()
},
mounted() {
if (this.form.email !== '') this.$refs.formValidator.validate()
if (this.form.identifier !== '') this.$refs.formValidator.validate()
},
}
</script>

View File

@ -31,15 +31,15 @@
class="pointer mb-3 bg-white appBoxShadow gradido-border-radius p-3 test-list-group-item"
>
<template #SEND>
<transaction-send v-bind="transactions[index]" v-on="$listeners" />
<transaction-send v-bind="transactions[index]" />
</template>
<template #RECEIVE>
<transaction-receive v-bind="transactions[index]" v-on="$listeners" />
<transaction-receive v-bind="transactions[index]" />
</template>
<template #CREATION>
<transaction-creation v-bind="transactions[index]" v-on="$listeners" />
<transaction-creation v-bind="transactions[index]" />
</template>
<template #LINK_SUMMARY>

View File

@ -32,11 +32,7 @@
<b-row>
<b-col>
<div class="font-weight-bold">
<name
:linkedUser="transaction.linkedUser"
v-on="$listeners"
fontColor="text-dark"
/>
<name :linkedUser="transaction.linkedUser" fontColor="text-dark" />
</div>
<div class="d-flex mt-3">
<div class="small">

View File

@ -3,9 +3,11 @@ import Name from './Name'
const localVue = global.localVue
const routerPushMock = jest.fn()
const mocks = {
$router: {
push: jest.fn(),
push: routerPushMock,
history: {
current: {
fullPath: '/transactions',
@ -47,7 +49,7 @@ describe('Name', () => {
describe('with linked user', () => {
beforeEach(async () => {
await wrapper.setProps({
linkedUser: { firstName: 'Bibi', lastName: 'Bloxberg', email: 'bibi@bloxberg.de' },
linkedUser: { firstName: 'Bibi', lastName: 'Bloxberg', gradidoID: 'gradido-ID' },
})
})
@ -64,13 +66,17 @@ describe('Name', () => {
await wrapper.find('div.gdd-transaction-list-item-name').find('a').trigger('click')
})
it('emits set tunneled email', () => {
expect(wrapper.emitted('set-tunneled-email')).toEqual([['bibi@bloxberg.de']])
it('pushes router to send', () => {
expect(routerPushMock).toBeCalledWith({
path: '/send',
})
})
it('pushes the route with query for email', () => {
expect(mocks.$router.push).toBeCalledWith({
path: '/send',
it('pushes query for gradidoID', () => {
expect(routerPushMock).toBeCalledWith({
query: {
gradidoID: 'gradido-ID',
},
})
})
})

View File

@ -1,7 +1,7 @@
<template>
<div class="name">
<div class="gdd-transaction-list-item-name">
<div v-if="linkedUser && linkedUser.email">
<div v-if="linkedUser && linkedUser.gradidoID">
<b-link @click.stop="tunnelEmail" :class="fontColor">
{{ itemText }}
</b-link>
@ -35,8 +35,8 @@ export default {
},
methods: {
tunnelEmail() {
this.$emit('set-tunneled-email', this.linkedUser.email)
if (this.$router.history.current.fullPath !== '/send') this.$router.push({ path: '/send' })
this.$router.push({ query: { gradidoID: this.linkedUser.gradidoID } })
},
},
computed: {

View File

@ -14,7 +14,6 @@
<div>
<name
class="font-weight-bold"
v-on="$listeners"
:amount="amount"
:linkedUser="linkedUser"
:linkId="linkId"

View File

@ -13,7 +13,6 @@
<div>
<name
class="font-weight-bold"
v-on="$listeners"
:amount="amount"
:linkedUser="linkedUser"
:linkId="linkId"

View File

@ -69,8 +69,8 @@ export const createUser = gql`
`
export const sendCoins = gql`
mutation($email: String!, $amount: Decimal!, $memo: String!) {
sendCoins(email: $email, amount: $amount, memo: $memo)
mutation($identifier: String!, $amount: Decimal!, $memo: String!) {
sendCoins(identifier: $identifier, amount: $amount, memo: $memo)
}
`
@ -144,6 +144,7 @@ export const createContributionMessage = gql`
export const login = gql`
mutation($email: String!, $password: String!, $publisherId: Int) {
login(email: $email, password: $password, publisherId: $publisherId) {
gradidoID
email
firstName
lastName

View File

@ -39,6 +39,7 @@ export const transactionsQuery = gql`
linkedUser {
firstName
lastName
gradidoID
email
}
decay {
@ -269,3 +270,12 @@ export const openCreations = gql`
}
}
`
export const user = gql`
query($identifier: String!) {
user(identifier: $identifier) {
firstName
lastName
}
}
`

View File

@ -174,15 +174,6 @@ describe('DashboardLayout', () => {
})
})
describe('set tunneled email', () => {
it('updates tunneled email', async () => {
await wrapper
.findComponent({ ref: 'router-view' })
.vm.$emit('set-tunneled-email', 'bibi@bloxberg.de')
expect(wrapper.vm.tunneledEmail).toBe('bibi@bloxberg.de')
})
})
it('has a component Navbar', () => {
expect(wrapper.findComponent({ name: 'Navbar' }).exists()).toBe(true)
})

View File

@ -127,7 +127,6 @@
:transactions="transactions"
:transactionCount="transactionCount"
:transactionLinkCount="transactionLinkCount"
@set-tunneled-email="setTunneledEmail"
/>
</template>
<template #community>
@ -149,7 +148,6 @@
:transactionLinkCount="transactionLinkCount"
:pending="pending"
@update-transactions="updateTransactions"
@set-tunneled-email="setTunneledEmail"
></router-view>
</fade-transition>
</div>
@ -164,7 +162,6 @@
:transactions="transactions"
:transactionCount="transactionCount"
:transactionLinkCount="transactionLinkCount"
@set-tunneled-email="setTunneledEmail"
/>
</template>
<template #empty />
@ -234,18 +231,12 @@ export default {
transactionLinkCount: 0,
pending: true,
visible: false,
tunneledEmail: null,
hamburger: true,
darkMode: false,
skeleton: true,
totalUsers: null,
}
},
provide() {
return {
getTunneledEmail: () => this.tunneledEmail,
}
},
created() {
this.updateTransactions(0)
this.getCommunityStatistics()
@ -319,9 +310,6 @@ export default {
setVisible(bool) {
this.visible = bool
},
setTunneledEmail(email) {
this.tunneledEmail = email
},
},
}
</script>

View File

@ -10,6 +10,7 @@ const apolloMutationMock = jest.fn()
apolloMutationMock.mockResolvedValue('success')
const navigatorClipboardMock = jest.fn()
const routerPushMock = jest.fn()
const localVue = global.localVue
@ -38,6 +39,9 @@ describe('Send', () => {
$route: {
query: {},
},
$router: {
push: routerPushMock,
},
}
const Wrapper = () => {
@ -85,8 +89,8 @@ describe('Send', () => {
it('shows the transaction formular again', () => {
expect(wrapper.findComponent({ name: 'TransactionForm' }).exists()).toBe(true)
})
// TODO:SKIPED at this point, a check must be made in the components ?
it.skip('restores the previous data in the formular', () => {
it('restores the previous data in the formular', () => {
expect(wrapper.find("input[type='email']").vm.$el.value).toBe('user@example.org')
expect(wrapper.find("input[type='text']").vm.$el.value).toBe('23.45')
expect(wrapper.find('textarea').vm.$el.value).toBe('Make the best of it!')
@ -107,10 +111,11 @@ describe('Send', () => {
expect.objectContaining({
mutation: sendCoins,
variables: {
email: 'user@example.org',
identifier: 'user@example.org',
amount: 23.45,
memo: 'Make the best of it!',
selected: SEND_TYPES.send,
userName: '',
},
}),
)
@ -162,6 +167,67 @@ describe('Send', () => {
})
})
describe('with gradidoID query', () => {
beforeEach(() => {
mocks.$route.query.gradidoID = 'gradido-ID'
wrapper = Wrapper()
})
it('has no email input field', () => {
expect(
wrapper.findComponent({ name: 'TransactionForm' }).find('input[type="email"]').exists(),
).toBe(false)
})
describe('submit form', () => {
beforeEach(async () => {
jest.clearAllMocks()
const transactionForm = wrapper.findComponent({ name: 'TransactionForm' })
await transactionForm.find('input[type="text"]').setValue('34.56')
await transactionForm.find('textarea').setValue('Make the best of it!')
await transactionForm.find('form').trigger('submit')
await flushPromises()
})
it('steps forward in the dialog', () => {
expect(wrapper.findComponent({ name: 'TransactionConfirmationSend' }).exists()).toBe(true)
})
describe('confirm transaction', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper
.findComponent({ name: 'TransactionConfirmationSend' })
.find('button.btn-gradido')
.trigger('click')
})
it('calls the API', async () => {
expect(apolloMutationMock).toBeCalledWith(
expect.objectContaining({
mutation: sendCoins,
variables: {
identifier: 'gradido-ID',
amount: 34.56,
memo: 'Make the best of it!',
selected: SEND_TYPES.send,
userName: '',
},
}),
)
})
it('resets the gradido ID query in route', () => {
expect(routerPushMock).toBeCalledWith({
query: {
gradidoID: undefined,
},
})
})
})
})
})
describe('transaction form link', () => {
const now = new Date().toISOString()
beforeEach(async () => {

View File

@ -11,9 +11,7 @@
<template #transactionConfirmationSend>
<transaction-confirmation-send
:balance="balance"
:email="transactionData.email"
:amount="transactionData.amount"
:memo="transactionData.memo"
v-bind="transactionData"
@send-transaction="sendTransaction"
@on-back="onBack"
></transaction-confirmation-send>
@ -21,7 +19,7 @@
<template #transactionConfirmationLink>
<transaction-confirmation-link
:balance="balance"
:email="transactionData.email"
:email="transactionData.identifier"
:amount="transactionData.amount"
:memo="transactionData.memo"
:loading="loading"
@ -62,7 +60,7 @@ import TransactionResultLink from '@/components/GddSend/TransactionResultLink'
import { sendCoins, createTransactionLink } from '@/graphql/mutations.js'
const EMPTY_TRANSACTION_DATA = {
email: '',
identifier: '',
amount: 0,
memo: '',
}
@ -168,6 +166,7 @@ export default {
throw new Error(`undefined transactionData.selected : ${this.transactionData.selected}`)
}
this.loading = false
this.$router.push({ query: { gradidoID: undefined } })
},
onBack() {
this.currentTransactionStep = TRANSACTION_STEPS.transactionForm

View File

@ -17,7 +17,6 @@
:showPagination="true"
:pageSize="pageSize"
@update-transactions="updateTransactions"
v-on="$listeners"
/>
</div>
</div>