Merge pull request #1859 from gradido/improve-apollo-logging

feat: 🍰 Improve Apollo Logging
This commit is contained in:
Moriz Wahl 2022-05-18 12:03:52 +02:00 committed by GitHub
commit 32e4a0cfd9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 466 additions and 140 deletions

View File

@ -528,7 +528,7 @@ jobs:
report_name: Coverage Backend report_name: Coverage Backend
type: lcov type: lcov
result_path: ./backend/coverage/lcov.info result_path: ./backend/coverage/lcov.info
min_coverage: 65 min_coverage: 64
token: ${{ github.token }} token: ${{ github.token }}
########################################################################## ##########################################################################

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
.dbeaver
.project
*.log *.log
/node_modules/* /node_modules/*
messages.pot messages.pot

View File

@ -4,5 +4,6 @@ module.exports = {
singleQuote: true, singleQuote: true,
trailingComma: "all", trailingComma: "all",
tabWidth: 2, tabWidth: 2,
bracketSpacing: true bracketSpacing: true,
endOfLine: "auto",
}; };

View File

@ -50,3 +50,7 @@ EMAIL_CODE_REQUEST_TIME=10
# Webhook # Webhook
WEBHOOK_ELOPAGE_SECRET=secret WEBHOOK_ELOPAGE_SECRET=secret
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
# LOG_LEVEL=info

View File

@ -5,4 +5,5 @@ module.exports = {
trailingComma: "all", trailingComma: "all",
tabWidth: 2, tabWidth: 2,
bracketSpacing: true, bracketSpacing: true,
endOfLine: "auto",
}; };

View File

@ -0,0 +1,66 @@
{
"appenders":
{
"access":
{
"type": "dateFile",
"filename": "../logs/backend/access.log",
"pattern": "%d{ISO8601} %p %c %X{user} %f:%l %m",
"keepFileExt" : true,
"fileNameSep" : "_"
},
"apollo":
{
"type": "dateFile",
"filename": "../logs/backend/apollo.log",
"pattern": "%d{ISO8601} %p %c %X{user} %f:%l %m",
"keepFileExt" : true,
"fileNameSep" : "_"
},
"errorFile":
{
"type": "dateFile",
"filename": "../logs/backend/errors.log",
"pattern": "%d{ISO8601} %p %c %X{user} %f:%l %m",
"keepFileExt" : true,
"fileNameSep" : "_"
},
"errors":
{
"type": "logLevelFilter",
"level": "error",
"appender": "errorFile"
},
"out":
{
"type": "stdout",
"layout":
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c %X{user} %f:%l %m"
}
}
},
"categories":
{
"default":
{
"appenders":
[
"out",
"apollo",
"errors"
],
"level": "debug",
"enableCallStack": true
},
"http":
{
"appenders":
[
"access"
],
"level": "info"
}
}
}

View File

@ -19,7 +19,6 @@
"dependencies": { "dependencies": {
"@types/jest": "^27.0.2", "@types/jest": "^27.0.2",
"@types/lodash.clonedeep": "^4.5.6", "@types/lodash.clonedeep": "^4.5.6",
"apollo-log": "^1.1.0",
"apollo-server-express": "^2.25.2", "apollo-server-express": "^2.25.2",
"apollo-server-testing": "^2.25.2", "apollo-server-testing": "^2.25.2",
"axios": "^0.21.1", "axios": "^0.21.1",
@ -33,6 +32,7 @@
"jest": "^27.2.4", "jest": "^27.2.4",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"log4js": "^6.4.6",
"mysql2": "^2.3.0", "mysql2": "^2.3.0",
"nodemailer": "^6.6.5", "nodemailer": "^6.6.5",
"random-bigint": "^0.0.1", "random-bigint": "^0.0.1",

View File

@ -1,10 +1,14 @@
import axios from 'axios' import axios from 'axios'
import { backendLogger as logger } from '@/server/logger'
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const apiPost = async (url: string, payload: unknown): Promise<any> => { export const apiPost = async (url: string, payload: unknown): Promise<any> => {
logger.trace('POST: url=' + url + ' payload=' + payload)
return axios return axios
.post(url, payload) .post(url, payload)
.then((result) => { .then((result) => {
logger.trace('POST-Response: result=' + result)
if (result.status !== 200) { if (result.status !== 200) {
throw new Error('HTTP Status Error ' + result.status) throw new Error('HTTP Status Error ' + result.status)
} }
@ -20,9 +24,11 @@ export const apiPost = async (url: string, payload: unknown): Promise<any> => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const apiGet = async (url: string): Promise<any> => { export const apiGet = async (url: string): Promise<any> => {
logger.trace('GET: url=' + url)
return axios return axios
.get(url) .get(url)
.then((result) => { .then((result) => {
logger.trace('GET-Response: result=' + result)
if (result.status !== 200) { if (result.status !== 200) {
throw new Error('HTTP Status Error ' + result.status) throw new Error('HTTP Status Error ' + result.status)
} }

View File

@ -12,6 +12,9 @@ Decimal.set({
const constants = { const constants = {
DB_VERSION: '0036-unique_previous_in_transactions', DB_VERSION: '0036-unique_previous_in_transactions',
DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0 DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
CONFIG_VERSION: { CONFIG_VERSION: {
DEFAULT: 'DEFAULT', DEFAULT: 'DEFAULT',
EXPECTED: 'v6.2022-04-21', EXPECTED: 'v6.2022-04-21',

View File

@ -1,3 +1,5 @@
import { backendLogger as logger } from '@/server/logger'
import { Context, getUser } from '@/server/context' import { Context, getUser } from '@/server/context'
import { Resolver, Query, Ctx, Authorized } from 'type-graphql' import { Resolver, Query, Ctx, Authorized } from 'type-graphql'
import { Balance } from '@model/Balance' import { Balance } from '@model/Balance'
@ -18,15 +20,22 @@ export class BalanceResolver {
const user = getUser(context) const user = getUser(context)
const now = new Date() const now = new Date()
logger.addContext('user', user.id)
logger.info(`balance(userId=${user.id})...`)
const gdtResolver = new GdtResolver() const gdtResolver = new GdtResolver()
const balanceGDT = await gdtResolver.gdtBalance(context) const balanceGDT = await gdtResolver.gdtBalance(context)
logger.debug(`balanceGDT=${balanceGDT}`)
const lastTransaction = context.lastTransaction const lastTransaction = context.lastTransaction
? context.lastTransaction ? context.lastTransaction
: await dbTransaction.findOne({ userId: user.id }, { order: { balanceDate: 'DESC' } }) : await dbTransaction.findOne({ userId: user.id }, { order: { balanceDate: 'DESC' } })
logger.debug(`lastTransaction=${lastTransaction}`)
// No balance found // No balance found
if (!lastTransaction) { if (!lastTransaction) {
logger.info(`no balance found, return Default-Balance!`)
return new Balance({ return new Balance({
balance: new Decimal(0), balance: new Decimal(0),
balanceGDT, balanceGDT,
@ -39,6 +48,8 @@ export class BalanceResolver {
context.transactionCount || context.transactionCount === 0 context.transactionCount || context.transactionCount === 0
? context.transactionCount ? context.transactionCount
: await dbTransaction.count({ where: { userId: user.id } }) : await dbTransaction.count({ where: { userId: user.id } })
logger.debug(`transactionCount=${count}`)
const linkCount = await dbTransactionLink.count({ const linkCount = await dbTransactionLink.count({
where: { where: {
userId: user.id, userId: user.id,
@ -46,6 +57,7 @@ export class BalanceResolver {
// validUntil: MoreThan(new Date()), // validUntil: MoreThan(new Date()),
}, },
}) })
logger.debug(`linkCount=${linkCount}`)
// The decay is always calculated on the last booked transaction // The decay is always calculated on the last booked transaction
const calculatedDecay = calculateDecay( const calculatedDecay = calculateDecay(
@ -53,6 +65,9 @@ export class BalanceResolver {
lastTransaction.balanceDate, lastTransaction.balanceDate,
now, now,
) )
logger.info(
`calculatedDecay(balance=${lastTransaction.balance}, balanceDate=${lastTransaction.balanceDate})=${calculatedDecay}`,
)
// The final balance is reduced by the link amount withheld // The final balance is reduced by the link amount withheld
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository) const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
@ -60,13 +75,27 @@ export class BalanceResolver {
? { sumHoldAvailableAmount: context.sumHoldAvailableAmount } ? { sumHoldAvailableAmount: context.sumHoldAvailableAmount }
: await transactionLinkRepository.summary(user.id, now) : await transactionLinkRepository.summary(user.id, now)
return new Balance({ logger.debug(`context.sumHoldAvailableAmount=${context.sumHoldAvailableAmount}`)
balance: calculatedDecay.balance logger.debug(`sumHoldAvailableAmount=${sumHoldAvailableAmount}`)
.minus(sumHoldAvailableAmount.toString())
.toDecimalPlaces(2, Decimal.ROUND_DOWN), // round towards zero const balance = calculatedDecay.balance
.minus(sumHoldAvailableAmount.toString())
.toDecimalPlaces(2, Decimal.ROUND_DOWN) // round towards zero
// const newBalance = new Balance({
// balance: calculatedDecay.balance
// .minus(sumHoldAvailableAmount.toString())
// .toDecimalPlaces(2, Decimal.ROUND_DOWN),
const newBalance = new Balance({
balance,
balanceGDT, balanceGDT,
count, count,
linkCount, linkCount,
}) })
logger.info(
`new Balance(balance=${balance}, balanceGDT=${balanceGDT}, count=${count}, linkCount=${linkCount}) = ${newBalance}`,
)
return newBalance
} }
} }

View File

@ -1,6 +1,7 @@
/* eslint-disable new-cap */ /* eslint-disable new-cap */
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-assertion */
import { backendLogger as logger } from '@/server/logger'
import CONFIG from '@/config' import CONFIG from '@/config'
import { Context, getUser } from '@/server/context' import { Context, getUser } from '@/server/context'
@ -44,15 +45,22 @@ export const executeTransaction = async (
recipient: dbUser, recipient: dbUser,
transactionLink?: dbTransactionLink | null, transactionLink?: dbTransactionLink | null,
): Promise<boolean> => { ): Promise<boolean> => {
logger.info(
`executeTransaction(amount=${amount}, memo=${memo}, sender=${sender}, recipient=${recipient})...`,
)
if (sender.id === recipient.id) { if (sender.id === recipient.id) {
logger.error(`Sender and Recipient are the same.`)
throw new Error('Sender and Recipient are the same.') throw new Error('Sender and Recipient are the same.')
} }
if (memo.length > MEMO_MAX_CHARS) { if (memo.length > MEMO_MAX_CHARS) {
logger.error(`memo text is too long: memo.length=${memo.length} > (${MEMO_MAX_CHARS}`)
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`) throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
} }
if (memo.length < MEMO_MIN_CHARS) { if (memo.length < MEMO_MIN_CHARS) {
logger.error(`memo text is too short: memo.length=${memo.length} < (${MEMO_MIN_CHARS}`)
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`) throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
} }
@ -64,13 +72,16 @@ export const executeTransaction = async (
receivedCallDate, receivedCallDate,
transactionLink, transactionLink,
) )
logger.debug(`calculated Balance=${sendBalance}`)
if (!sendBalance) { if (!sendBalance) {
logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`)
throw new Error("user hasn't enough GDD or amount is < 0") throw new Error("user hasn't enough GDD or amount is < 0")
} }
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect() await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED') await queryRunner.startTransaction('READ UNCOMMITTED')
logger.debug(`open Transaction to write...`)
try { try {
// transaction // transaction
const transactionSend = new dbTransaction() const transactionSend = new dbTransaction()
@ -87,6 +98,8 @@ export const executeTransaction = async (
transactionSend.transactionLinkId = transactionLink ? transactionLink.id : null transactionSend.transactionLinkId = transactionLink ? transactionLink.id : null
await queryRunner.manager.insert(dbTransaction, transactionSend) await queryRunner.manager.insert(dbTransaction, transactionSend)
logger.debug(`sendTransaction inserted: ${dbTransaction}`)
const transactionReceive = new dbTransaction() const transactionReceive = new dbTransaction()
transactionReceive.typeId = TransactionTypeId.RECEIVE transactionReceive.typeId = TransactionTypeId.RECEIVE
transactionReceive.memo = memo transactionReceive.memo = memo
@ -102,12 +115,15 @@ export const executeTransaction = async (
transactionReceive.linkedTransactionId = transactionSend.id transactionReceive.linkedTransactionId = transactionSend.id
transactionReceive.transactionLinkId = transactionLink ? transactionLink.id : null transactionReceive.transactionLinkId = transactionLink ? transactionLink.id : null
await queryRunner.manager.insert(dbTransaction, transactionReceive) await queryRunner.manager.insert(dbTransaction, transactionReceive)
logger.debug(`receive Transaction inserted: ${dbTransaction}`)
// Save linked transaction id for send // Save linked transaction id for send
transactionSend.linkedTransactionId = transactionReceive.id transactionSend.linkedTransactionId = transactionReceive.id
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend) await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
logger.debug(`send Transaction updated: ${transactionSend}`)
if (transactionLink) { if (transactionLink) {
logger.info(`transactionLink: ${transactionLink}`)
transactionLink.redeemedAt = receivedCallDate transactionLink.redeemedAt = receivedCallDate
transactionLink.redeemedBy = recipient.id transactionLink.redeemedBy = recipient.id
await queryRunner.manager.update( await queryRunner.manager.update(
@ -118,13 +134,15 @@ export const executeTransaction = async (
} }
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
logger.info(`commit Transaction successful...`)
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
logger.error(`Transaction was not successful: ${e}`)
throw new Error(`Transaction was not successful: ${e}`) throw new Error(`Transaction was not successful: ${e}`)
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }
logger.debug(`prepare Email for transaction received...`)
// send notification email // send notification email
// TODO: translate // TODO: translate
await sendTransactionReceivedEmail({ await sendTransactionReceivedEmail({
@ -138,7 +156,7 @@ export const executeTransaction = async (
memo, memo,
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
}) })
logger.info(`finished executeTransaction successfully`)
return true return true
} }
@ -154,16 +172,21 @@ export class TransactionResolver {
const now = new Date() const now = new Date()
const user = getUser(context) const user = getUser(context)
logger.addContext('user', user.id)
logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.email})`)
// find current balance // find current balance
const lastTransaction = await dbTransaction.findOne( const lastTransaction = await dbTransaction.findOne(
{ userId: user.id }, { userId: user.id },
{ order: { balanceDate: 'DESC' } }, { order: { balanceDate: 'DESC' } },
) )
logger.debug(`lastTransaction=${lastTransaction}`)
const balanceResolver = new BalanceResolver() const balanceResolver = new BalanceResolver()
context.lastTransaction = lastTransaction context.lastTransaction = lastTransaction
if (!lastTransaction) { if (!lastTransaction) {
logger.info('no lastTransaction')
return new TransactionList(await balanceResolver.balance(context), []) return new TransactionList(await balanceResolver.balance(context), [])
} }
@ -186,6 +209,8 @@ export class TransactionResolver {
involvedUserIds.push(transaction.linkedUserId) involvedUserIds.push(transaction.linkedUserId)
} }
}) })
logger.debug(`involvedUserIds=${involvedUserIds}`)
// We need to show the name for deleted users for old transactions // We need to show the name for deleted users for old transactions
const involvedDbUsers = await dbUser const involvedDbUsers = await dbUser
.createQueryBuilder() .createQueryBuilder()
@ -193,6 +218,7 @@ export class TransactionResolver {
.where('id IN (:...userIds)', { userIds: involvedUserIds }) .where('id IN (:...userIds)', { userIds: involvedUserIds })
.getMany() .getMany()
const involvedUsers = involvedDbUsers.map((u) => new User(u)) const involvedUsers = involvedDbUsers.map((u) => new User(u))
logger.debug(`involvedUsers=${involvedUsers}`)
const self = new User(user) const self = new User(user)
const transactions: Transaction[] = [] const transactions: Transaction[] = []
@ -201,10 +227,13 @@ export class TransactionResolver {
const { sumHoldAvailableAmount, sumAmount, lastDate, firstDate, transactionLinkcount } = const { sumHoldAvailableAmount, sumAmount, lastDate, firstDate, transactionLinkcount } =
await transactionLinkRepository.summary(user.id, now) await transactionLinkRepository.summary(user.id, now)
context.linkCount = transactionLinkcount context.linkCount = transactionLinkcount
logger.debug(`transactionLinkcount=${transactionLinkcount}`)
context.sumHoldAvailableAmount = sumHoldAvailableAmount context.sumHoldAvailableAmount = sumHoldAvailableAmount
logger.debug(`sumHoldAvailableAmount=${sumHoldAvailableAmount}`)
// decay & link transactions // decay & link transactions
if (currentPage === 1 && order === Order.DESC) { if (currentPage === 1 && order === Order.DESC) {
logger.debug(`currentPage == 1: transactions=${transactions}`)
// The virtual decay is always on the booked amount, not including the generated, not yet booked links, // The virtual decay is always on the booked amount, not including the generated, not yet booked links,
// since the decay is substantially different when the amount is less // since the decay is substantially different when the amount is less
transactions.push( transactions.push(
@ -216,8 +245,11 @@ export class TransactionResolver {
sumHoldAvailableAmount, sumHoldAvailableAmount,
), ),
) )
logger.debug(`transactions=${transactions}`)
// virtual transaction for pending transaction-links sum // virtual transaction for pending transaction-links sum
if (sumHoldAvailableAmount.greaterThan(0)) { if (sumHoldAvailableAmount.greaterThan(0)) {
logger.debug(`sumHoldAvailableAmount > 0: transactions=${transactions}`)
transactions.push( transactions.push(
virtualLinkTransaction( virtualLinkTransaction(
lastTransaction.balance.minus(sumHoldAvailableAmount.toString()), lastTransaction.balance.minus(sumHoldAvailableAmount.toString()),
@ -229,6 +261,7 @@ export class TransactionResolver {
self, self,
), ),
) )
logger.debug(`transactions=${transactions}`)
} }
} }
@ -240,6 +273,7 @@ export class TransactionResolver {
: involvedUsers.find((u) => u.id === userTransaction.linkedUserId) : involvedUsers.find((u) => u.id === userTransaction.linkedUserId)
transactions.push(new Transaction(userTransaction, self, linkedUser)) transactions.push(new Transaction(userTransaction, self, linkedUser))
}) })
logger.debug(`TransactionTypeId.CREATION: transactions=${transactions}`)
// Construct Result // Construct Result
return new TransactionList(await balanceResolver.balance(context), transactions) return new TransactionList(await balanceResolver.balance(context), transactions)
@ -251,29 +285,38 @@ export class TransactionResolver {
@Args() { email, amount, memo }: TransactionSendArgs, @Args() { email, amount, memo }: TransactionSendArgs,
@Ctx() context: Context, @Ctx() context: Context,
): Promise<boolean> { ): Promise<boolean> {
logger.info(`sendCoins(email=${email}, amount=${amount}, memo=${memo})`)
// TODO this is subject to replay attacks // TODO this is subject to replay attacks
const senderUser = getUser(context) const senderUser = getUser(context)
if (senderUser.pubKey.length !== 32) { if (senderUser.pubKey.length !== 32) {
logger.error(`invalid sender public key:${senderUser.pubKey}`)
throw new Error('invalid sender public key') throw new Error('invalid sender public key')
} }
// validate recipient user // validate recipient user
const recipientUser = await dbUser.findOne({ email: email }, { withDeleted: true }) const recipientUser = await dbUser.findOne({ email: email }, { withDeleted: true })
if (!recipientUser) { if (!recipientUser) {
logger.error(`recipient not known: email=${email}`)
throw new Error('recipient not known') throw new Error('recipient not known')
} }
if (recipientUser.deletedAt) { if (recipientUser.deletedAt) {
logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`)
throw new Error('The recipient account was deleted') throw new Error('The recipient account was deleted')
} }
if (!recipientUser.emailChecked) { if (!recipientUser.emailChecked) {
logger.error(`The recipient account is not activated: recipientUser=${recipientUser}`)
throw new Error('The recipient account is not activated') throw new Error('The recipient account is not activated')
} }
if (!isHexPublicKey(recipientUser.pubKey.toString('hex'))) { if (!isHexPublicKey(recipientUser.pubKey.toString('hex'))) {
logger.error(`invalid recipient public key: recipientUser=${recipientUser}`)
throw new Error('invalid recipient public key') throw new Error('invalid recipient public key')
} }
await executeTransaction(amount, memo, senderUser, recipientUser) await executeTransaction(amount, memo, senderUser, recipientUser)
logger.info(
`successful executeTransaction(amount=${amount}, memo=${memo}, senderUser=${senderUser}, recipientUser=${recipientUser})`,
)
return true return true
} }
} }

View File

@ -14,6 +14,8 @@ import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail' import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
import { printTimeDuration, activationLink } from './UserResolver' import { printTimeDuration, activationLink } from './UserResolver'
import { logger } from '@test/testSetup'
// import { klicktippSignIn } from '@/apis/KlicktippController' // import { klicktippSignIn } from '@/apis/KlicktippController'
jest.mock('@/mailer/sendAccountActivationEmail', () => { jest.mock('@/mailer/sendAccountActivationEmail', () => {
@ -43,7 +45,7 @@ let mutate: any, query: any, con: any
let testEnv: any let testEnv: any
beforeAll(async () => { beforeAll(async () => {
testEnv = await testEnvironment() testEnv = await testEnvironment(logger)
mutate = testEnv.mutate mutate = testEnv.mutate
query = testEnv.query query = testEnv.query
con = testEnv.con con = testEnv.con
@ -149,12 +151,14 @@ describe('UserResolver', () => {
}) })
describe('email already exists', () => { describe('email already exists', () => {
it('throws an error', async () => { it('throws and logs an error', async () => {
await expect(mutate({ mutation: createUser, variables })).resolves.toEqual( const mutation = await mutate({ mutation: createUser, variables })
expect(mutation).toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('User already exists.')], errors: [new GraphQLError('User already exists.')],
}), }),
) )
expect(logger.error).toBeCalledWith('User already exists with this email=peter@lustig.de')
}) })
}) })

View File

@ -1,4 +1,6 @@
import fs from 'fs' import fs from 'fs'
import { backendLogger as logger } from '@/server/logger'
import { Context, getUser } from '@/server/context' import { Context, getUser } from '@/server/context'
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql' import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
import { getConnection, getCustomRepository } from '@dbTools/typeorm' import { getConnection, getCustomRepository } from '@dbTools/typeorm'
@ -43,6 +45,7 @@ const WORDS = fs
.toString() .toString()
.split(',') .split(',')
const PassphraseGenerate = (): string[] => { const PassphraseGenerate = (): string[] => {
logger.trace('PassphraseGenerate...')
const result = [] const result = []
for (let i = 0; i < PHRASE_WORD_COUNT; i++) { for (let i = 0; i < PHRASE_WORD_COUNT; i++) {
result.push(WORDS[sodium.randombytes_random() % 2048]) result.push(WORDS[sodium.randombytes_random() % 2048])
@ -51,7 +54,9 @@ const PassphraseGenerate = (): string[] => {
} }
const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => { const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => {
logger.trace('KeyPairEd25519Create...')
if (!passphrase.length || passphrase.length < PHRASE_WORD_COUNT) { if (!passphrase.length || passphrase.length < PHRASE_WORD_COUNT) {
logger.error('passphrase empty or to short')
throw new Error('passphrase empty or to short') throw new Error('passphrase empty or to short')
} }
@ -79,14 +84,19 @@ const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => {
privKey, privKey,
outputHashBuffer.slice(0, sodium.crypto_sign_SEEDBYTES), outputHashBuffer.slice(0, sodium.crypto_sign_SEEDBYTES),
) )
logger.debug(`KeyPair creation ready. pubKey=${pubKey}`)
return [pubKey, privKey] return [pubKey, privKey]
} }
const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[] => { const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[] => {
logger.trace('SecretKeyCryptographyCreateKey...')
const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex') const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex')
const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex') const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex')
if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) { if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) {
logger.error(
`ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
)
throw new Error( throw new Error(
`ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`, `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
) )
@ -115,39 +125,50 @@ const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[
const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES) const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES)
sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey) sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey)
logger.debug(
`SecretKeyCryptographyCreateKey...successful: encryptionKeyHash= ${encryptionKeyHash}, encryptionKey= ${encryptionKey}`,
)
return [encryptionKeyHash, encryptionKey] return [encryptionKeyHash, encryptionKey]
} }
const getEmailHash = (email: string): Buffer => { const getEmailHash = (email: string): Buffer => {
logger.trace('getEmailHash...')
const emailHash = Buffer.alloc(sodium.crypto_generichash_BYTES) const emailHash = Buffer.alloc(sodium.crypto_generichash_BYTES)
sodium.crypto_generichash(emailHash, Buffer.from(email)) sodium.crypto_generichash(emailHash, Buffer.from(email))
logger.debug(`getEmailHash...successful: ${emailHash}`)
return emailHash return emailHash
} }
const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => { const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => {
logger.trace('SecretKeyCryptographyEncrypt...')
const encrypted = Buffer.alloc(message.length + sodium.crypto_secretbox_MACBYTES) const encrypted = Buffer.alloc(message.length + sodium.crypto_secretbox_MACBYTES)
const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES) const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES)
nonce.fill(31) // static nonce nonce.fill(31) // static nonce
sodium.crypto_secretbox_easy(encrypted, message, nonce, encryptionKey) sodium.crypto_secretbox_easy(encrypted, message, nonce, encryptionKey)
logger.debug(`SecretKeyCryptographyEncrypt...successful: ${encrypted}`)
return encrypted return encrypted
} }
const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: Buffer): Buffer => { const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: Buffer): Buffer => {
logger.trace('SecretKeyCryptographyDecrypt...')
const message = Buffer.alloc(encryptedMessage.length - sodium.crypto_secretbox_MACBYTES) const message = Buffer.alloc(encryptedMessage.length - sodium.crypto_secretbox_MACBYTES)
const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES) const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES)
nonce.fill(31) // static nonce nonce.fill(31) // static nonce
sodium.crypto_secretbox_open_easy(message, encryptedMessage, nonce, encryptionKey) sodium.crypto_secretbox_open_easy(message, encryptedMessage, nonce, encryptionKey)
logger.debug(`SecretKeyCryptographyDecrypt...successful: ${message}`)
return message return message
} }
const newEmailOptIn = (userId: number): LoginEmailOptIn => { const newEmailOptIn = (userId: number): LoginEmailOptIn => {
logger.trace('newEmailOptIn...')
const emailOptIn = new LoginEmailOptIn() const emailOptIn = new LoginEmailOptIn()
emailOptIn.verificationCode = random(64) emailOptIn.verificationCode = random(64)
emailOptIn.userId = userId emailOptIn.userId = userId
emailOptIn.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER emailOptIn.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER
logger.debug(`newEmailOptIn...successful: ${emailOptIn}`)
return emailOptIn return emailOptIn
} }
@ -159,8 +180,14 @@ export const checkOptInCode = async (
userId: number, userId: number,
optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER, optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER,
): Promise<LoginEmailOptIn> => { ): Promise<LoginEmailOptIn> => {
logger.info(`checkOptInCode... ${optInCode}`)
if (optInCode) { if (optInCode) {
if (!canResendOptIn(optInCode)) { if (!canResendOptIn(optInCode)) {
logger.error(
`email already sent less than ${printTimeDuration(
CONFIG.EMAIL_CODE_REQUEST_TIME,
)} minutes ago`,
)
throw new Error( throw new Error(
`email already sent less than ${printTimeDuration( `email already sent less than ${printTimeDuration(
CONFIG.EMAIL_CODE_REQUEST_TIME, CONFIG.EMAIL_CODE_REQUEST_TIME,
@ -170,16 +197,20 @@ export const checkOptInCode = async (
optInCode.updatedAt = new Date() optInCode.updatedAt = new Date()
optInCode.resendCount++ optInCode.resendCount++
} else { } else {
logger.trace('create new OptIn for userId=' + userId)
optInCode = newEmailOptIn(userId) optInCode = newEmailOptIn(userId)
} }
optInCode.emailOptInTypeId = optInType optInCode.emailOptInTypeId = optInType
await LoginEmailOptIn.save(optInCode).catch(() => { await LoginEmailOptIn.save(optInCode).catch(() => {
logger.error('Unable to save optin code= ' + optInCode)
throw new Error('Unable to save optin code.') throw new Error('Unable to save optin code.')
}) })
logger.debug(`checkOptInCode...successful: ${optInCode} for userid=${userId}`)
return optInCode return optInCode
} }
export const activationLink = (optInCode: LoginEmailOptIn): string => { export const activationLink = (optInCode: LoginEmailOptIn): string => {
logger.debug(`activationLink(${LoginEmailOptIn})...`)
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, optInCode.verificationCode.toString()) return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, optInCode.verificationCode.toString())
} }
@ -189,6 +220,7 @@ export class UserResolver {
@Query(() => User) @Query(() => User)
@UseMiddleware(klicktippNewsletterStateMiddleware) @UseMiddleware(klicktippNewsletterStateMiddleware)
async verifyLogin(@Ctx() context: Context): Promise<User> { async verifyLogin(@Ctx() context: Context): Promise<User> {
logger.info('verifyLogin...')
// TODO refactor and do not have duplicate code with login(see below) // TODO refactor and do not have duplicate code with login(see below)
const userEntity = getUser(context) const userEntity = getUser(context)
const user = new User(userEntity) const user = new User(userEntity)
@ -201,10 +233,11 @@ export class UserResolver {
const coinanimation = await userSettingRepository const coinanimation = await userSettingRepository
.readBoolean(userEntity.id, Setting.COIN_ANIMATION) .readBoolean(userEntity.id, Setting.COIN_ANIMATION)
.catch((error) => { .catch((error) => {
logger.error('error:', error)
throw new Error(error) throw new Error(error)
}) })
user.coinanimation = coinanimation user.coinanimation = coinanimation
logger.debug(`verifyLogin... successful: ${user.firstName}.${user.lastName}, ${user.email}`)
return user return user
} }
@ -215,34 +248,46 @@ export class UserResolver {
@Args() { email, password, publisherId }: UnsecureLoginArgs, @Args() { email, password, publisherId }: UnsecureLoginArgs,
@Ctx() context: Context, @Ctx() context: Context,
): Promise<User> { ): Promise<User> {
logger.info(`login with ${email}, ***, ${publisherId} ...`)
email = email.trim().toLowerCase() email = email.trim().toLowerCase()
const dbUser = await DbUser.findOneOrFail({ email }, { withDeleted: true }).catch(() => { const dbUser = await DbUser.findOneOrFail({ email }, { withDeleted: true }).catch(() => {
logger.error(`User with email=${email} does not exists`)
throw new Error('No user with this credentials') throw new Error('No user with this credentials')
}) })
if (dbUser.deletedAt) { if (dbUser.deletedAt) {
logger.error('The User was permanently deleted in database.')
throw new Error('This user was permanently deleted. Contact support for questions.') throw new Error('This user was permanently deleted. Contact support for questions.')
} }
if (!dbUser.emailChecked) { if (!dbUser.emailChecked) {
logger.error('The Users email is not validate yet.')
throw new Error('User email not validated') throw new Error('User email not validated')
} }
if (dbUser.password === BigInt(0)) { if (dbUser.password === BigInt(0)) {
logger.error('The User has not set a password yet.')
// TODO we want to catch this on the frontend and ask the user to check his emails or resend code // TODO we want to catch this on the frontend and ask the user to check his emails or resend code
throw new Error('User has no password set yet') throw new Error('User has no password set yet')
} }
if (!dbUser.pubKey || !dbUser.privKey) { if (!dbUser.pubKey || !dbUser.privKey) {
logger.error('The User has no private or publicKey.')
// TODO we want to catch this on the frontend and ask the user to check his emails or resend code // TODO we want to catch this on the frontend and ask the user to check his emails or resend code
throw new Error('User has no private or publicKey') throw new Error('User has no private or publicKey')
} }
const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
const loginUserPassword = BigInt(dbUser.password.toString()) const loginUserPassword = BigInt(dbUser.password.toString())
if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) { if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) {
logger.error('The User has no valid credentials.')
throw new Error('No user with this credentials') throw new Error('No user with this credentials')
} }
// add pubKey in logger-context for layout-pattern X{user} to print it in each logging message
logger.addContext('user', dbUser.id)
logger.debug('login credentials valid...')
const user = new User(dbUser) const user = new User(dbUser)
logger.debug('user=' + user)
// Elopage Status & Stored PublisherId // Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage({ ...context, user: dbUser }) user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
logger.info('user.hasElopage=' + user.hasElopage)
if (!user.hasElopage && publisherId) { if (!user.hasElopage && publisherId) {
user.publisherId = publisherId user.publisherId = publisherId
dbUser.publisherId = publisherId dbUser.publisherId = publisherId
@ -262,7 +307,7 @@ export class UserResolver {
key: 'token', key: 'token',
value: encode(dbUser.pubKey), value: encode(dbUser.pubKey),
}) })
logger.info('successful Login:' + user)
return user return user
} }
@ -274,6 +319,9 @@ export class UserResolver {
// The functionality is fully client side - the client just needs to delete his token with the current implementation. // The functionality is fully client side - the client just needs to delete his token with the current implementation.
// we could try to force this by sending `token: null` or `token: ''` with this call. But since it bares no real security // we could try to force this by sending `token: null` or `token: ''` with this call. But since it bares no real security
// we should just return true for now. // we should just return true for now.
logger.info('Logout...')
// remove user.pubKey from logger-context to ensure a correct filter on log-messages belonging to the same user
logger.addContext('user', 'unknown')
return true return true
} }
@ -283,6 +331,9 @@ export class UserResolver {
@Args() @Args()
{ email, firstName, lastName, language, publisherId, redeemCode = null }: CreateUserArgs, { email, firstName, lastName, language, publisherId, redeemCode = null }: CreateUserArgs,
): Promise<User> { ): Promise<User> {
logger.info(
`createUser(email=${email}, firstName=${firstName}, lastName=${lastName}, language=${language}, publisherId=${publisherId}, redeemCode =${redeemCode})`,
)
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field? // TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
// default int publisher_id = 0; // default int publisher_id = 0;
@ -295,7 +346,9 @@ export class UserResolver {
email = email.trim().toLowerCase() email = email.trim().toLowerCase()
// TODO we cannot use repository.count(), since it does not allow to specify if you want to include the soft deletes // TODO we cannot use repository.count(), since it does not allow to specify if you want to include the soft deletes
const userFound = await DbUser.findOne({ email }, { withDeleted: true }) const userFound = await DbUser.findOne({ email }, { withDeleted: true })
logger.info(`DbUser.findOne(email=${email}) = ${userFound}`)
if (userFound) { if (userFound) {
logger.error('User already exists with this email=' + email)
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent. // TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
throw new Error(`User already exists.`) throw new Error(`User already exists.`)
} }
@ -314,8 +367,10 @@ export class UserResolver {
dbUser.language = language dbUser.language = language
dbUser.publisherId = publisherId dbUser.publisherId = publisherId
dbUser.passphrase = passphrase.join(' ') dbUser.passphrase = passphrase.join(' ')
logger.debug('new dbUser=' + dbUser)
if (redeemCode) { if (redeemCode) {
const transactionLink = await dbTransactionLink.findOne({ code: redeemCode }) const transactionLink = await dbTransactionLink.findOne({ code: redeemCode })
logger.info('redeemCode found transactionLink=' + transactionLink)
if (transactionLink) { if (transactionLink) {
dbUser.referrerId = transactionLink.userId dbUser.referrerId = transactionLink.userId
} }
@ -332,15 +387,13 @@ export class UserResolver {
await queryRunner.startTransaction('READ UNCOMMITTED') await queryRunner.startTransaction('READ UNCOMMITTED')
try { try {
await queryRunner.manager.save(dbUser).catch((error) => { await queryRunner.manager.save(dbUser).catch((error) => {
// eslint-disable-next-line no-console logger.error('Error while saving dbUser', error)
console.log('Error while saving dbUser', error)
throw new Error('error saving user') throw new Error('error saving user')
}) })
const emailOptIn = newEmailOptIn(dbUser.id) const emailOptIn = newEmailOptIn(dbUser.id)
await queryRunner.manager.save(emailOptIn).catch((error) => { await queryRunner.manager.save(emailOptIn).catch((error) => {
// eslint-disable-next-line no-console logger.error('Error while saving emailOptIn', error)
console.log('Error while saving emailOptIn', error)
throw new Error('error saving email opt in') throw new Error('error saving email opt in')
}) })
@ -357,31 +410,35 @@ export class UserResolver {
email, email,
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME), duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
}) })
logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`)
/* uncomment this, when you need the activation link on the console /* uncomment this, when you need the activation link on the console */
// In case EMails are disabled log the activation link for the user // In case EMails are disabled log the activation link for the user
if (!emailSent) { if (!emailSent) {
// eslint-disable-next-line no-console logger.debug(`Account confirmation link: ${activationLink}`)
console.log(`Account confirmation link: ${activationLink}`)
} }
*/
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
} catch (e) { } catch (e) {
logger.error(`error during create user with ${e}`)
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
throw e throw e
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }
logger.info('createUser() successful...')
return new User(dbUser) return new User(dbUser)
} }
@Authorized([RIGHTS.SEND_RESET_PASSWORD_EMAIL]) @Authorized([RIGHTS.SEND_RESET_PASSWORD_EMAIL])
@Mutation(() => Boolean) @Mutation(() => Boolean)
async forgotPassword(@Arg('email') email: string): Promise<boolean> { async forgotPassword(@Arg('email') email: string): Promise<boolean> {
logger.info(`forgotPassword(${email})...`)
email = email.trim().toLowerCase() email = email.trim().toLowerCase()
const user = await DbUser.findOne({ email }) const user = await DbUser.findOne({ email })
if (!user) return true if (!user) {
logger.warn(`no user found with ${email}`)
return true
}
// can be both types: REGISTER and RESET_PASSWORD // can be both types: REGISTER and RESET_PASSWORD
let optInCode = await LoginEmailOptIn.findOne({ let optInCode = await LoginEmailOptIn.findOne({
@ -389,7 +446,7 @@ export class UserResolver {
}) })
optInCode = await checkOptInCode(optInCode, user.id, OptInType.EMAIL_OPT_IN_RESET_PASSWORD) optInCode = await checkOptInCode(optInCode, user.id, OptInType.EMAIL_OPT_IN_RESET_PASSWORD)
logger.info(`optInCode for ${email}=${optInCode}`)
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const emailSent = await sendResetPasswordEmailMailer({ const emailSent = await sendResetPasswordEmailMailer({
link: activationLink(optInCode), link: activationLink(optInCode),
@ -399,13 +456,12 @@ export class UserResolver {
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME), duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
}) })
/* uncomment this, when you need the activation link on the console /* uncomment this, when you need the activation link on the console */
// In case EMails are disabled log the activation link for the user // In case EMails are disabled log the activation link for the user
if (!emailSent) { if (!emailSent) {
// eslint-disable-next-line no-console logger.debug(`Reset password link: ${activationLink(optInCode)}`)
console.log(`Reset password link: ${link}`)
} }
*/ logger.info(`forgotPassword(${email}) successful...`)
return true return true
} }
@ -416,6 +472,7 @@ export class UserResolver {
@Arg('code') code: string, @Arg('code') code: string,
@Arg('password') password: string, @Arg('password') password: string,
): Promise<boolean> { ): Promise<boolean> {
logger.info(`setPassword(${code}, ***)...`)
// Validate Password // Validate Password
if (!isPassword(password)) { if (!isPassword(password)) {
throw new Error( throw new Error(
@ -425,34 +482,44 @@ export class UserResolver {
// Load code // Load code
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => { const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => {
logger.error('Could not login with emailVerificationCode')
throw new Error('Could not login with emailVerificationCode') throw new Error('Could not login with emailVerificationCode')
}) })
logger.debug('optInCode loaded...')
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
if (!isOptInValid(optInCode)) { if (!isOptInValid(optInCode)) {
logger.error(
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
)
throw new Error( throw new Error(
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`, `email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
) )
} }
logger.debug('optInCode is valid...')
// load user // load user
const user = await DbUser.findOneOrFail({ id: optInCode.userId }).catch(() => { const user = await DbUser.findOneOrFail({ id: optInCode.userId }).catch(() => {
logger.error('Could not find corresponding Login User')
throw new Error('Could not find corresponding Login User') throw new Error('Could not find corresponding Login User')
}) })
logger.debug('user with optInCode found...')
// Generate Passphrase if needed // Generate Passphrase if needed
if (!user.passphrase) { if (!user.passphrase) {
const passphrase = PassphraseGenerate() const passphrase = PassphraseGenerate()
user.passphrase = passphrase.join(' ') user.passphrase = passphrase.join(' ')
logger.debug('new Passphrase generated...')
} }
const passphrase = user.passphrase.split(' ') const passphrase = user.passphrase.split(' ')
if (passphrase.length < PHRASE_WORD_COUNT) { if (passphrase.length < PHRASE_WORD_COUNT) {
logger.error('Could not load a correct passphrase')
// TODO if this can happen we cannot recover from that // TODO if this can happen we cannot recover from that
// this seem to be good on production data, if we dont // this seem to be good on production data, if we dont
// make a coding mistake we do not have a problem here // make a coding mistake we do not have a problem here
throw new Error('Could not load a correct passphrase') throw new Error('Could not load a correct passphrase')
} }
logger.debug('Passphrase is valid...')
// Activate EMail // Activate EMail
user.emailChecked = true user.emailChecked = true
@ -464,6 +531,7 @@ export class UserResolver {
user.password = passwordHash[0].readBigUInt64LE() // using the shorthash user.password = passwordHash[0].readBigUInt64LE() // using the shorthash
user.pubKey = keyPair[0] user.pubKey = keyPair[0]
user.privKey = encryptedPrivkey user.privKey = encryptedPrivkey
logger.debug('User credentials updated ...')
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect() await queryRunner.connect()
@ -472,12 +540,15 @@ export class UserResolver {
try { try {
// Save user // Save user
await queryRunner.manager.save(user).catch((error) => { await queryRunner.manager.save(user).catch((error) => {
logger.error('error saving user: ' + error)
throw new Error('error saving user: ' + error) throw new Error('error saving user: ' + error)
}) })
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
logger.info('User data written successfully...')
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
logger.error('Error on writing User data:' + e)
throw e throw e
} finally { } finally {
await queryRunner.release() await queryRunner.release()
@ -488,7 +559,11 @@ export class UserResolver {
if (optInCode.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) { if (optInCode.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) {
try { try {
await klicktippSignIn(user.email, user.language, user.firstName, user.lastName) await klicktippSignIn(user.email, user.language, user.firstName, user.lastName)
} catch { logger.debug(
`klicktippSignIn(${user.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
)
} catch (e) {
logger.error('Error subscribe to klicktipp:' + e)
// TODO is this a problem? // TODO is this a problem?
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
/* uncomment this, when you need the activation link on the console /* uncomment this, when you need the activation link on the console
@ -503,13 +578,19 @@ export class UserResolver {
@Authorized([RIGHTS.QUERY_OPT_IN]) @Authorized([RIGHTS.QUERY_OPT_IN])
@Query(() => Boolean) @Query(() => Boolean)
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> { async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
logger.info(`queryOptIn(${optIn})...`)
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: optIn }) const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: optIn })
logger.debug(`found optInCode=${optInCode}`)
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
if (!isOptInValid(optInCode)) { if (!isOptInValid(optInCode)) {
logger.error(
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
)
throw new Error( throw new Error(
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`, `email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
) )
} }
logger.info(`queryOptIn(${optIn}) successful...`)
return true return true
} }
@ -520,6 +601,9 @@ export class UserResolver {
{ firstName, lastName, language, password, passwordNew, coinanimation }: UpdateUserInfosArgs, { firstName, lastName, language, password, passwordNew, coinanimation }: UpdateUserInfosArgs,
@Ctx() context: Context, @Ctx() context: Context,
): Promise<boolean> { ): Promise<boolean> {
logger.info(
`updateUserInfos(${firstName}, ${lastName}, ${language}, ***, ***, ${coinanimation})...`,
)
const userEntity = getUser(context) const userEntity = getUser(context)
if (firstName) { if (firstName) {
@ -532,6 +616,7 @@ export class UserResolver {
if (language) { if (language) {
if (!isLanguage(language)) { if (!isLanguage(language)) {
logger.error(`"${language}" isn't a valid language`)
throw new Error(`"${language}" isn't a valid language`) throw new Error(`"${language}" isn't a valid language`)
} }
userEntity.language = language userEntity.language = language
@ -540,6 +625,7 @@ export class UserResolver {
if (password && passwordNew) { if (password && passwordNew) {
// Validate Password // Validate Password
if (!isPassword(passwordNew)) { if (!isPassword(passwordNew)) {
logger.error('newPassword does not fullfil the rules')
throw new Error( throw new Error(
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!', 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
) )
@ -548,13 +634,16 @@ export class UserResolver {
// TODO: This had some error cases defined - like missing private key. This is no longer checked. // TODO: This had some error cases defined - like missing private key. This is no longer checked.
const oldPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, password) const oldPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, password)
if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) { if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) {
logger.error(`Old password is invalid`)
throw new Error(`Old password is invalid`) throw new Error(`Old password is invalid`)
} }
const privKey = SecretKeyCryptographyDecrypt(userEntity.privKey, oldPasswordHash[1]) const privKey = SecretKeyCryptographyDecrypt(userEntity.privKey, oldPasswordHash[1])
logger.debug('oldPassword decrypted...')
const newPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, passwordNew) // return short and long hash const newPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, passwordNew) // return short and long hash
logger.debug('newPasswordHash created...')
const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1]) const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1])
logger.debug('PrivateKey encrypted...')
// Save new password hash and newly encrypted private key // Save new password hash and newly encrypted private key
userEntity.password = newPasswordHash[0].readBigUInt64LE() userEntity.password = newPasswordHash[0].readBigUInt64LE()
@ -580,25 +669,30 @@ export class UserResolver {
}) })
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
logger.debug('writing User data successful...')
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
logger.error(`error on writing updated user data: ${e}`)
throw e throw e
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }
logger.info('updateUserInfos() successfully finished...')
return true return true
} }
@Authorized([RIGHTS.HAS_ELOPAGE]) @Authorized([RIGHTS.HAS_ELOPAGE])
@Query(() => Boolean) @Query(() => Boolean)
async hasElopage(@Ctx() context: Context): Promise<boolean> { async hasElopage(@Ctx() context: Context): Promise<boolean> {
logger.info(`hasElopage()...`)
const userEntity = context.user const userEntity = context.user
if (!userEntity) { if (!userEntity) {
logger.info('missing context.user for EloPage-check')
return false return false
} }
const elopageBuys = hasElopageBuys(userEntity.email)
return hasElopageBuys(userEntity.email) logger.debug(`has ElopageBuys = ${elopageBuys}`)
return elopageBuys
} }
} }

View File

@ -2,6 +2,8 @@ import { sendEMail } from './sendEMail'
import { createTransport } from 'nodemailer' import { createTransport } from 'nodemailer'
import CONFIG from '@/config' import CONFIG from '@/config'
import { logger } from '@test/testSetup'
CONFIG.EMAIL = false CONFIG.EMAIL = false
CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL' CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL'
CONFIG.EMAIL_SMTP_PORT = '1234' CONFIG.EMAIL_SMTP_PORT = '1234'
@ -26,11 +28,6 @@ jest.mock('nodemailer', () => {
describe('sendEMail', () => { describe('sendEMail', () => {
let result: boolean let result: boolean
describe('config email is false', () => { describe('config email is false', () => {
// eslint-disable-next-line no-console
const consoleLog = console.log
const consoleLogMock = jest.fn()
// eslint-disable-next-line no-console
console.log = consoleLogMock
beforeEach(async () => { beforeEach(async () => {
result = await sendEMail({ result = await sendEMail({
to: 'receiver@mail.org', to: 'receiver@mail.org',
@ -39,13 +36,8 @@ describe('sendEMail', () => {
}) })
}) })
afterAll(() => { it('logs warining', () => {
// eslint-disable-next-line no-console expect(logger.info).toBeCalledWith('Emails are disabled via config...')
console.log = consoleLog
})
it('logs warining to console', () => {
expect(consoleLogMock).toBeCalledWith('Emails are disabled via config')
}) })
it('returns false', () => { it('returns false', () => {

View File

@ -1,3 +1,4 @@
import { backendLogger as logger } from '@/server/logger'
import { createTransport } from 'nodemailer' import { createTransport } from 'nodemailer'
import CONFIG from '@/config' import CONFIG from '@/config'
@ -7,9 +8,10 @@ export const sendEMail = async (emailDef: {
subject: string subject: string
text: string text: string
}): Promise<boolean> => { }): Promise<boolean> => {
logger.info(`send Email: to=${emailDef.to}, subject=${emailDef.subject}, text=${emailDef.text}`)
if (!CONFIG.EMAIL) { if (!CONFIG.EMAIL) {
// eslint-disable-next-line no-console logger.info(`Emails are disabled via config...`)
console.log('Emails are disabled via config')
return false return false
} }
const transporter = createTransport({ const transporter = createTransport({
@ -27,7 +29,9 @@ export const sendEMail = async (emailDef: {
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
}) })
if (!info.messageId) { if (!info.messageId) {
logger.error('error sending notification email, but transaction succeed')
throw new Error('error sending notification email, but transaction succeed') throw new Error('error sending notification email, but transaction succeed')
} }
logger.info('send Email successfully.')
return true return true
} }

View File

@ -1,3 +1,4 @@
import { backendLogger as logger } from '@/server/logger'
import Decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'
import { sendEMail } from './sendEMail' import { sendEMail } from './sendEMail'
import { transactionReceived } from './text/transactionReceived' import { transactionReceived } from './text/transactionReceived'
@ -13,6 +14,12 @@ export const sendTransactionReceivedEmail = (data: {
memo: string memo: string
overviewURL: string overviewURL: string
}): Promise<boolean> => { }): Promise<boolean> => {
logger.info(
`sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName},
<${data.email}>,
subject=${transactionReceived.de.subject},
text=${transactionReceived.de.text(data)}`,
)
return sendEMail({ return sendEMail({
to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`, to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`,
subject: transactionReceived.de.subject, subject: transactionReceived.de.subject,

View File

@ -22,22 +22,32 @@ import schema from '@/graphql/schema'
import { elopageWebhook } from '@/webhook/elopage' import { elopageWebhook } from '@/webhook/elopage'
import { Connection } from '@dbTools/typeorm' import { Connection } from '@dbTools/typeorm'
import { apolloLogger } from './logger'
import { Logger } from 'log4js'
// TODO implement // TODO implement
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity"; // import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
type ServerDef = { apollo: ApolloServer; app: Express; con: Connection } type ServerDef = { apollo: ApolloServer; app: Express; con: Connection }
// eslint-disable-next-line @typescript-eslint/no-explicit-any const createServer = async (
const createServer = async (context: any = serverContext): Promise<ServerDef> => { // eslint-disable-next-line @typescript-eslint/no-explicit-any
context: any = serverContext,
logger: Logger = apolloLogger,
): Promise<ServerDef> => {
logger.debug('createServer...')
// open mysql connection // open mysql connection
const con = await connection() const con = await connection()
if (!con || !con.isConnected) { if (!con || !con.isConnected) {
logger.fatal(`Couldn't open connection to database!`)
throw new Error(`Fatal: Couldn't open connection to database`) throw new Error(`Fatal: Couldn't open connection to database`)
} }
// check for correct database version // check for correct database version
const dbVersion = await checkDBVersion(CONFIG.DB_VERSION) const dbVersion = await checkDBVersion(CONFIG.DB_VERSION)
if (!dbVersion) { if (!dbVersion) {
logger.fatal('Fatal: Database Version incorrect')
throw new Error('Fatal: Database Version incorrect') throw new Error('Fatal: Database Version incorrect')
} }
@ -62,8 +72,10 @@ const createServer = async (context: any = serverContext): Promise<ServerDef> =>
introspection: CONFIG.GRAPHIQL, introspection: CONFIG.GRAPHIQL,
context, context,
plugins, plugins,
logger,
}) })
apollo.applyMiddleware({ app, path: '/' }) apollo.applyMiddleware({ app, path: '/' })
logger.debug('createServer...successful')
return { apollo, app, con } return { apollo, app, con }
} }

View File

@ -0,0 +1,18 @@
import log4js from 'log4js'
import CONFIG from '@/config'
import { readFileSync } from 'fs'
const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8'))
options.categories.default.level = CONFIG.LOG_LEVEL
log4js.configure(options)
const apolloLogger = log4js.getLogger('apollo')
const backendLogger = log4js.getLogger('backend')
apolloLogger.addContext('user', 'unknown')
backendLogger.addContext('user', 'unknown')
export { apolloLogger, backendLogger }

View File

@ -1,8 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ApolloLogPlugin, LogMutateData } from 'apollo-log' import clonedeep from 'lodash.clonedeep'
import cloneDeep from 'lodash.clonedeep'
const setHeadersPlugin = { const setHeadersPlugin = {
requestDidStart() { requestDidStart() {
@ -22,24 +21,35 @@ const setHeadersPlugin = {
}, },
} }
const apolloLogPlugin = ApolloLogPlugin({ const filterVariables = (variables: any) => {
mutate: (data: LogMutateData) => { const vars = clonedeep(variables)
// We need to deep clone the object in order to not modify the actual request if (vars.password) vars.password = '***'
const dataCopy = cloneDeep(data) if (vars.passwordNew) vars.passwordNew = '***'
return vars
}
// mask password if part of the query const logPlugin = {
if (dataCopy.context.request.variables && dataCopy.context.request.variables.password) { requestDidStart(requestContext: any) {
dataCopy.context.request.variables.password = '***' const { logger } = requestContext
const { query, mutation, variables } = requestContext.request
logger.trace(`Request:
${mutation || query}variables: ${JSON.stringify(filterVariables(variables), null, 2)}`)
return {
willSendResponse(requestContext: any) {
if (requestContext.context.user) logger.trace(`User ID: ${requestContext.context.user.id}`)
if (requestContext.response.data)
logger.trace(`Response-Data:
${JSON.stringify(requestContext.response.data, null, 2)}`)
if (requestContext.response.errors)
logger.trace(`Response-Errors:
${JSON.stringify(requestContext.response.errors, null, 2)}`)
return requestContext
},
} }
// mask token at all times
dataCopy.context.context.token = '***'
return dataCopy
}, },
}) }
const plugins = const plugins =
process.env.NODE_ENV === 'development' ? [setHeadersPlugin] : [setHeadersPlugin, apolloLogPlugin] process.env.NODE_ENV === 'development' ? [setHeadersPlugin] : [setHeadersPlugin, logPlugin]
export default plugins export default plugins

View File

@ -1,12 +1,12 @@
import { Migration } from '@entity/Migration' import { Migration } from '@entity/Migration'
import { backendLogger as logger } from '@/server/logger'
const getDBVersion = async (): Promise<string | null> => { const getDBVersion = async (): Promise<string | null> => {
try { try {
const dbVersion = await Migration.findOne({ order: { version: 'DESC' } }) const dbVersion = await Migration.findOne({ order: { version: 'DESC' } })
return dbVersion ? dbVersion.fileName : null return dbVersion ? dbVersion.fileName : null
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console logger.error(error)
console.log(error)
return null return null
} }
} }
@ -14,8 +14,7 @@ const getDBVersion = async (): Promise<string | null> => {
const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => { const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => {
const dbVersion = await getDBVersion() const dbVersion = await getDBVersion()
if (!dbVersion || dbVersion.indexOf(DB_VERSION) === -1) { if (!dbVersion || dbVersion.indexOf(DB_VERSION) === -1) {
// eslint-disable-next-line no-console logger.error(
console.log(
`Wrong database version detected - the backend requires '${DB_VERSION}' but found '${ `Wrong database version detected - the backend requires '${DB_VERSION}' but found '${
dbVersion || 'None' dbVersion || 'None'
}`, }`,

View File

@ -25,8 +25,8 @@ export const cleanDB = async () => {
} }
} }
export const testEnvironment = async () => { export const testEnvironment = async (logger?: any) => {
const server = await createServer(context) const server = await createServer(context, logger)
const con = server.con const con = server.con
const testClient = createTestClient(server.apollo) const testClient = createTestClient(server.apollo)
const mutate = testClient.mutate const mutate = testClient.mutate

View File

@ -1,7 +1,22 @@
/* eslint-disable no-console */ import { backendLogger as logger } from '@/server/logger'
// disable console.info for apollo log
// eslint-disable-next-line @typescript-eslint/no-empty-function
console.info = () => {}
jest.setTimeout(1000000) jest.setTimeout(1000000)
jest.mock('@/server/logger', () => {
const originalModule = jest.requireActual('@/server/logger')
return {
__esModule: true,
...originalModule,
backendLogger: {
addContext: jest.fn(),
trace: jest.fn(),
debug: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
error: jest.fn(),
fatal: jest.fn(),
},
}
})
export { logger }

View File

@ -2,7 +2,7 @@
# yarn lockfile v1 # yarn lockfile v1
"@apollo/protobufjs@1.2.2", "@apollo/protobufjs@^1.0.3": "@apollo/protobufjs@1.2.2":
version "1.2.2" version "1.2.2"
resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.2.2.tgz#4bd92cd7701ccaef6d517cdb75af2755f049f87c" resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.2.2.tgz#4bd92cd7701ccaef6d517cdb75af2755f049f87c"
integrity sha512-vF+zxhPiLtkwxONs6YanSt1EpwpGilThpneExUN5K3tCymuxNnVq2yojTvnpRjv2QfsEIt/n7ozPIIzBLwGIDQ== integrity sha512-vF+zxhPiLtkwxONs6YanSt1EpwpGilThpneExUN5K3tCymuxNnVq2yojTvnpRjv2QfsEIt/n7ozPIIzBLwGIDQ==
@ -1265,24 +1265,6 @@ apollo-link@^1.2.14:
tslib "^1.9.3" tslib "^1.9.3"
zen-observable-ts "^0.8.21" zen-observable-ts "^0.8.21"
apollo-log@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/apollo-log/-/apollo-log-1.1.0.tgz#e21287c917cf735b77adc06f07034f965e9b24de"
integrity sha512-TciLu+85LSqk7t7ZGKrYN5jFiCcRMLujBjrLiOQGHGgVVkvmKlwK0oELSS9kiHQIhTq23p8qVVWb08spLpQ7Jw==
dependencies:
apollo-server-plugin-base "^0.10.4"
chalk "^4.1.0"
fast-safe-stringify "^2.0.7"
loglevelnext "^4.0.1"
nanoid "^3.1.20"
apollo-reporting-protobuf@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/apollo-reporting-protobuf/-/apollo-reporting-protobuf-0.6.2.tgz#5572866be9b77f133916532b10e15fbaa4158304"
integrity sha512-WJTJxLM+MRHNUxt1RTl4zD0HrLdH44F2mDzMweBj1yHL0kSt8I1WwoiF/wiGVSpnG48LZrBegCaOJeuVbJTbtw==
dependencies:
"@apollo/protobufjs" "^1.0.3"
apollo-reporting-protobuf@^0.8.0: apollo-reporting-protobuf@^0.8.0:
version "0.8.0" version "0.8.0"
resolved "https://registry.yarnpkg.com/apollo-reporting-protobuf/-/apollo-reporting-protobuf-0.8.0.tgz#ae9d967934d3d8ed816fc85a0d8068ef45c371b9" resolved "https://registry.yarnpkg.com/apollo-reporting-protobuf/-/apollo-reporting-protobuf-0.8.0.tgz#ae9d967934d3d8ed816fc85a0d8068ef45c371b9"
@ -1290,13 +1272,6 @@ apollo-reporting-protobuf@^0.8.0:
dependencies: dependencies:
"@apollo/protobufjs" "1.2.2" "@apollo/protobufjs" "1.2.2"
apollo-server-caching@^0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.5.3.tgz#cf42a77ad09a46290a246810075eaa029b5305e1"
integrity sha512-iMi3087iphDAI0U2iSBE9qtx9kQoMMEWr6w+LwXruBD95ek9DWyj7OeC2U/ngLjRsXM43DoBDXlu7R+uMjahrQ==
dependencies:
lru-cache "^6.0.0"
apollo-server-caching@^0.7.0: apollo-server-caching@^0.7.0:
version "0.7.0" version "0.7.0"
resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.7.0.tgz#e6d1e68e3bb571cba63a61f60b434fb771c6ff39" resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.7.0.tgz#e6d1e68e3bb571cba63a61f60b434fb771c6ff39"
@ -1335,7 +1310,7 @@ apollo-server-core@^2.25.2:
subscriptions-transport-ws "^0.9.19" subscriptions-transport-ws "^0.9.19"
uuid "^8.0.0" uuid "^8.0.0"
apollo-server-env@^3.0.0, apollo-server-env@^3.1.0: apollo-server-env@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-3.1.0.tgz#0733c2ef50aea596cc90cf40a53f6ea2ad402cd0" resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-3.1.0.tgz#0733c2ef50aea596cc90cf40a53f6ea2ad402cd0"
integrity sha512-iGdZgEOAuVop3vb0F2J3+kaBVi4caMoxefHosxmgzAbbSpvWehB8Y1QiSyyMeouYC38XNVk5wnZl+jdGSsWsIQ== integrity sha512-iGdZgEOAuVop3vb0F2J3+kaBVi4caMoxefHosxmgzAbbSpvWehB8Y1QiSyyMeouYC38XNVk5wnZl+jdGSsWsIQ==
@ -1371,13 +1346,6 @@ apollo-server-express@^2.25.2:
subscriptions-transport-ws "^0.9.19" subscriptions-transport-ws "^0.9.19"
type-is "^1.6.16" type-is "^1.6.16"
apollo-server-plugin-base@^0.10.4:
version "0.10.4"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.10.4.tgz#fbf73f64f95537ca9f9639dd7c535eb5eeb95dcd"
integrity sha512-HRhbyHgHFTLP0ImubQObYhSgpmVH4Rk1BinnceZmwudIVLKrqayIVOELdyext/QnSmmzg5W7vF3NLGBcVGMqDg==
dependencies:
apollo-server-types "^0.6.3"
apollo-server-plugin-base@^0.13.0: apollo-server-plugin-base@^0.13.0:
version "0.13.0" version "0.13.0"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.13.0.tgz#3f85751a420d3c4625355b6cb3fbdd2acbe71f13" resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.13.0.tgz#3f85751a420d3c4625355b6cb3fbdd2acbe71f13"
@ -1392,15 +1360,6 @@ apollo-server-testing@^2.25.2:
dependencies: dependencies:
apollo-server-core "^2.25.2" apollo-server-core "^2.25.2"
apollo-server-types@^0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.6.3.tgz#f7aa25ff7157863264d01a77d7934aa6e13399e8"
integrity sha512-aVR7SlSGGY41E1f11YYz5bvwA89uGmkVUtzMiklDhZ7IgRJhysT5Dflt5IuwDxp+NdQkIhVCErUXakopocFLAg==
dependencies:
apollo-reporting-protobuf "^0.6.2"
apollo-server-caching "^0.5.3"
apollo-server-env "^3.0.0"
apollo-server-types@^0.9.0: apollo-server-types@^0.9.0:
version "0.9.0" version "0.9.0"
resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.9.0.tgz#ccf550b33b07c48c72f104fbe2876232b404848b" resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.9.0.tgz#ccf550b33b07c48c72f104fbe2876232b404848b"
@ -1952,6 +1911,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0" whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0" whatwg-url "^8.0.0"
date-format@^4.0.9:
version "4.0.9"
resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.9.tgz#4788015ac56dedebe83b03bc361f00c1ddcf1923"
integrity sha512-+8J+BOUpSrlKLQLeF8xJJVTxS8QfRSuJgwxSVvslzgO3E6khbI0F5mMEPf5mTYhCCm4h99knYP6H3W9n3BQFrg==
debug@2.6.9, debug@^2.2.0, debug@^2.6.9: debug@2.6.9, debug@^2.2.0, debug@^2.6.9:
version "2.6.9" version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@ -1973,6 +1937,13 @@ debug@^3.2.6, debug@^3.2.7:
dependencies: dependencies:
ms "^2.1.1" ms "^2.1.1"
debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"
decimal.js-light@^2.5.1: decimal.js-light@^2.5.1:
version "2.5.1" version "2.5.1"
resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
@ -2558,11 +2529,6 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
fast-safe-stringify@^2.0.7:
version "2.1.1"
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
fastq@^1.6.0: fastq@^1.6.0:
version "1.13.0" version "1.13.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
@ -2632,6 +2598,11 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561"
integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA== integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==
flatted@^3.2.5:
version "3.2.5"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3"
integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==
follow-redirects@^1.14.0: follow-redirects@^1.14.0:
version "1.14.4" version "1.14.4"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379"
@ -2668,6 +2639,15 @@ fs-capacitor@^2.0.4:
resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.4.tgz#5a22e72d40ae5078b4fe64fe4d08c0d3fc88ad3c" resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.4.tgz#5a22e72d40ae5078b4fe64fe4d08c0d3fc88ad3c"
integrity sha512-8S4f4WsCryNw2mJJchi46YgB6CR5Ze+4L1h8ewl9tEpL4SJ3ZO+c/bS4BWhB8bK+O3TMqhuZarTitd0S0eh2pA== integrity sha512-8S4f4WsCryNw2mJJchi46YgB6CR5Ze+4L1h8ewl9tEpL4SJ3ZO+c/bS4BWhB8bK+O3TMqhuZarTitd0S0eh2pA==
fs-extra@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==
dependencies:
graceful-fs "^4.2.0"
jsonfile "^6.0.1"
universalify "^2.0.0"
fs.realpath@^1.0.0: fs.realpath@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@ -2818,6 +2798,11 @@ graceful-fs@^4.1.2, graceful-fs@^4.2.4:
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
version "4.2.10"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
graphql-extensions@^0.15.0: graphql-extensions@^0.15.0:
version "0.15.0" version "0.15.0"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.15.0.tgz#3f291f9274876b0c289fa4061909a12678bd9817" resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.15.0.tgz#3f291f9274876b0c289fa4061909a12678bd9817"
@ -3810,6 +3795,15 @@ json5@^1.0.1:
dependencies: dependencies:
minimist "^1.2.0" minimist "^1.2.0"
jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
dependencies:
universalify "^2.0.0"
optionalDependencies:
graceful-fs "^4.1.6"
jsonwebtoken@^8.5.1: jsonwebtoken@^8.5.1:
version "8.5.1" version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
@ -3978,16 +3972,22 @@ lodash@4.x, lodash@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
log4js@^6.4.6:
version "6.4.6"
resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.4.6.tgz#1878aa3f09973298ecb441345fe9dd714e355c15"
integrity sha512-1XMtRBZszmVZqPAOOWczH+Q94AI42mtNWjvjA5RduKTSWjEc56uOBbyM1CJnfN4Ym0wSd8cQ43zOojlSHgRDAw==
dependencies:
date-format "^4.0.9"
debug "^4.3.4"
flatted "^3.2.5"
rfdc "^1.3.0"
streamroller "^3.0.8"
loglevel@^1.6.7: loglevel@^1.6.7:
version "1.7.1" version "1.7.1"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw== integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==
loglevelnext@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-4.0.1.tgz#4406c6348c243a35272ac75d7d8e4e60ecbcd011"
integrity sha512-/tlMUn5wqgzg9msy0PiWc+8fpVXEuYPq49c2RGyw2NAh0hSrgq6j/Z3YPnwWsILMoFJ+ZT6ePHnWUonkjDnq2Q==
long@^4.0.0: long@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
@ -4150,11 +4150,6 @@ named-placeholders@^1.1.2:
dependencies: dependencies:
lru-cache "^4.1.3" lru-cache "^4.1.3"
nanoid@^3.1.20:
version "3.1.32"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.32.tgz#8f96069e6239cc0a9ae8c0d3b41a3b4933a88c0a"
integrity sha512-F8mf7R3iT9bvThBoW4tGXhXFHCctyCiUUPrWF8WaTqa3h96d9QybkSeba43XVOOE3oiLfkVDe4bT8MeGmkrTxw==
natural-compare@^1.4.0: natural-compare@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@ -4746,6 +4741,11 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rfdc@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
rimraf@^3.0.0, rimraf@^3.0.2: rimraf@^3.0.0, rimraf@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
@ -4981,6 +4981,15 @@ stack-utils@^2.0.3:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
streamroller@^3.0.8:
version "3.0.8"
resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.0.8.tgz#84b190e4080ee311ca1ebe0444e30ac8eedd028d"
integrity sha512-VI+ni3czbFZrd1MrlybxykWZ8sMDCMtTU7YJyhgb9M5X6d1DDxLdJr+gSnmRpXPMnIWxWKMaAE8K0WumBp3lDg==
dependencies:
date-format "^4.0.9"
debug "^4.3.4"
fs-extra "^10.1.0"
streamsearch@0.1.2: streamsearch@0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
@ -5363,6 +5372,11 @@ universalify@^0.1.2:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
universalify@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
unpipe@1.0.0, unpipe@~1.0.0: unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"

View File

@ -5,4 +5,5 @@ module.exports = {
trailingComma: "all", trailingComma: "all",
tabWidth: 2, tabWidth: 2,
bracketSpacing: true, bracketSpacing: true,
endOfLine: "auto",
}; };

View File

@ -4,5 +4,6 @@ module.exports = {
singleQuote: true, singleQuote: true,
trailingComma: "all", trailingComma: "all",
tabWidth: 2, tabWidth: 2,
bracketSpacing: true bracketSpacing: true,
endOfLine: "auto",
}; };