Merge branch 'master' into 2594-contributions-list-frontend

This commit is contained in:
Alexander Friedland 2023-02-02 16:57:30 +01:00 committed by GitHub
commit 0f449de4a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 352 additions and 211 deletions

View File

@ -4,6 +4,7 @@ import path from 'path'
import { createTransport } from 'nodemailer' import { createTransport } from 'nodemailer'
import Email from 'email-templates' import Email from 'email-templates'
import i18n from 'i18n' import i18n from 'i18n'
import LogError from '@/server/LogError'
export const sendEmailTranslated = async (params: { export const sendEmailTranslated = async (params: {
receiver: { receiver: {
@ -73,8 +74,7 @@ export const sendEmailTranslated = async (params: {
logger.info('Result: ', result) logger.info('Result: ', result)
}) })
.catch((error: unknown) => { .catch((error: unknown) => {
logger.error('Error sending notification email: ', error) throw new LogError('Error sending notification email', error)
throw new Error('Error sending notification email!')
}) })
i18n.setLocale(rememberLocaleToRestore) i18n.setLocale(rememberLocaleToRestore)

View File

@ -180,14 +180,14 @@ export class ContributionResolver {
@Args() @Args()
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated, { currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
@Arg('statusFilter', () => [ContributionStatus], { nullable: true }) @Arg('statusFilter', () => [ContributionStatus], { nullable: true })
statusFilters?: ContributionStatus[], statusFilter?: ContributionStatus[],
): Promise<ContributionListResult> { ): Promise<ContributionListResult> {
const where: { const where: {
contributionStatus?: FindOperator<string> | null contributionStatus?: FindOperator<string> | null
} = {} } = {}
if (statusFilters && statusFilters.length) { if (statusFilter && statusFilter.length) {
where.contributionStatus = In(statusFilters) where.contributionStatus = In(statusFilter)
} }
const [dbContributions, count] = await getConnection() const [dbContributions, count] = await getConnection()

View File

@ -75,7 +75,7 @@ describe('EmailOptinCodes', () => {
query({ query: queryOptIn, variables: { optIn: optinCode } }), query({ query: queryOptIn, variables: { optIn: optinCode } }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
data: null, data: null,
errors: [new GraphQLError('email was sent more than 24 hours ago')], errors: [new GraphQLError('Email was sent more than 24 hours ago')],
}) })
}) })
@ -84,7 +84,7 @@ describe('EmailOptinCodes', () => {
mutate({ mutation: setPassword, variables: { code: optinCode, password: 'Aa12345_' } }), mutate({ mutation: setPassword, variables: { code: optinCode, password: 'Aa12345_' } }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
data: null, data: null,
errors: [new GraphQLError('email was sent more than 24 hours ago')], errors: [new GraphQLError('Email was sent more than 24 hours ago')],
}) })
}) })
}) })
@ -96,7 +96,7 @@ describe('EmailOptinCodes', () => {
mutate({ mutation: forgotPassword, variables: { email: 'peter@lustig.de' } }), mutate({ mutation: forgotPassword, variables: { email: 'peter@lustig.de' } }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
data: null, data: null,
errors: [new GraphQLError('email already sent less than 10 minutes ago')], errors: [new GraphQLError('Email already sent less than 10 minutes ago')],
}) })
}) })

View File

@ -89,7 +89,7 @@ describe('send coins', () => {
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(`UserContact with email=wrong@email.com does not exists`) expect(logger.error).toBeCalledWith('No user with this credentials', 'wrong@email.com')
}) })
describe('deleted recipient', () => { describe('deleted recipient', () => {

View File

@ -549,7 +549,9 @@ describe('UserResolver', () => {
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Password entered is lexically invalid') expect(logger.error).toBeCalledWith(
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
)
}) })
}) })
@ -606,9 +608,7 @@ describe('UserResolver', () => {
}) })
it('logs the error found', () => { it('logs the error found', () => {
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith('No user with this credentials', variables.email)
'UserContact with email=bibi@bloxberg.de does not exists',
)
}) })
}) })
@ -668,7 +668,112 @@ describe('UserResolver', () => {
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('The User has no valid credentials.') expect(logger.error).toBeCalledWith('No user with this credentials', variables.email)
})
})
describe('user is in database but deleted', () => {
beforeAll(async () => {
jest.clearAllMocks()
await userFactory(testEnv, stephenHawking)
const variables = {
email: stephenHawking.email,
password: 'Aa12345_',
publisherId: 1234,
}
result = await mutate({ mutation: login, variables })
})
afterAll(async () => {
await cleanDB()
})
it('returns an error', () => {
expect(result).toEqual(
expect.objectContaining({
errors: [
new GraphQLError('This user was permanently deleted. Contact support for questions'),
],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'This user was permanently deleted. Contact support for questions',
expect.objectContaining({
firstName: stephenHawking.firstName,
lastName: stephenHawking.lastName,
}),
)
})
})
describe('user is in database but email not confirmed', () => {
beforeAll(async () => {
jest.clearAllMocks()
await userFactory(testEnv, garrickOllivander)
const variables = {
email: garrickOllivander.email,
password: 'Aa12345_',
publisherId: 1234,
}
result = await mutate({ mutation: login, variables })
})
afterAll(async () => {
await cleanDB()
})
it('returns an error', () => {
expect(result).toEqual(
expect.objectContaining({
errors: [new GraphQLError('The Users email is not validate yet')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'The Users email is not validate yet',
expect.objectContaining({
firstName: garrickOllivander.firstName,
lastName: garrickOllivander.lastName,
}),
)
})
})
describe.skip('user is in database but password is not set', () => {
beforeAll(async () => {
jest.clearAllMocks()
// TODO: we need an user without password set
const user = await userFactory(testEnv, bibiBloxberg)
user.password = BigInt(0)
await user.save()
result = await mutate({ mutation: login, variables })
})
afterAll(async () => {
await cleanDB()
})
it('returns an error', () => {
expect(result).toEqual(
expect.objectContaining({
errors: [new GraphQLError('The User has not set a password yet')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'The User has not set a password yet',
expect.objectContaining({
firstName: bibiBloxberg.firstName,
lastName: bibiBloxberg.lastName,
}),
)
}) })
}) })
}) })
@ -828,7 +933,7 @@ describe('UserResolver', () => {
expect.objectContaining({ expect.objectContaining({
errors: [ errors: [
new GraphQLError( new GraphQLError(
`email already sent less than ${printTimeDuration( `Email already sent less than ${printTimeDuration(
CONFIG.EMAIL_CODE_REQUEST_TIME, CONFIG.EMAIL_CODE_REQUEST_TIME,
)} ago`, )} ago`,
), ),
@ -870,13 +975,13 @@ describe('UserResolver', () => {
CONFIG.EMAIL_CODE_REQUEST_TIME = emailCodeRequestTime CONFIG.EMAIL_CODE_REQUEST_TIME = emailCodeRequestTime
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual( await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('email already sent less than 10 minutes ago')], errors: [new GraphQLError('Email already sent less than 10 minutes ago')],
}), }),
) )
}) })
it('logs the error found', () => { it('logs the error found', () => {
expect(logger.error).toBeCalledWith(`email already sent less than 10 minutes ago`) expect(logger.error).toBeCalledWith(`Email already sent less than 10 minutes ago`)
}) })
}) })
}) })
@ -1001,13 +1106,13 @@ describe('UserResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError(`"not-valid" isn't a valid language`)], errors: [new GraphQLError('Given language is not a valid language')],
}), }),
) )
}) })
it('logs the error found', () => { it('logs the error found', () => {
expect(logger.error).toBeCalledWith(`"not-valid" isn't a valid language`) expect(logger.error).toBeCalledWith('Given language is not a valid language', 'not-valid')
}) })
}) })
@ -1058,7 +1163,9 @@ describe('UserResolver', () => {
}) })
it('logs the error found', () => { it('logs the error found', () => {
expect(logger.error).toBeCalledWith('newPassword does not fullfil the rules') expect(logger.error).toBeCalledWith(
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
)
}) })
}) })
@ -1116,7 +1223,9 @@ describe('UserResolver', () => {
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('The User has no valid credentials.') expect(logger.error).toBeCalledWith(
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
)
}) })
}) })
}) })
@ -1322,13 +1431,13 @@ describe('UserResolver', () => {
mutate({ mutation: setUserRole, variables: { userId: admin.id + 1, isAdmin: true } }), mutate({ mutation: setUserRole, variables: { userId: admin.id + 1, isAdmin: true } }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError(`Could not find user with userId: ${admin.id + 1}`)], errors: [new GraphQLError('Could not find user with given ID')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(`Could not find user with userId: ${admin.id + 1}`) expect(logger.error).toBeCalledWith('Could not find user with given ID', admin.id + 1)
}) })
}) })
@ -1379,12 +1488,12 @@ describe('UserResolver', () => {
mutate({ mutation: setUserRole, variables: { userId: admin.id, isAdmin: false } }), mutate({ mutation: setUserRole, variables: { userId: admin.id, isAdmin: false } }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('Administrator can not change his own role!')], errors: [new GraphQLError('Administrator can not change his own role')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Administrator can not change his own role!') expect(logger.error).toBeCalledWith('Administrator can not change his own role')
}) })
}) })
@ -1400,13 +1509,13 @@ describe('UserResolver', () => {
mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: true } }), mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: true } }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('User is already admin!')], errors: [new GraphQLError('User is already admin')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('User is already admin!') expect(logger.error).toBeCalledWith('User is already admin')
}) })
}) })
@ -1421,13 +1530,13 @@ describe('UserResolver', () => {
mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: false } }), mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: false } }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('User is already a usual user!')], errors: [new GraphQLError('User is already an usual user')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('User is already a usual user!') expect(logger.error).toBeCalledWith('User is already an usual user')
}) })
}) })
}) })
@ -1494,13 +1603,13 @@ describe('UserResolver', () => {
mutate({ mutation: deleteUser, variables: { userId: admin.id + 1 } }), mutate({ mutation: deleteUser, variables: { userId: admin.id + 1 } }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError(`Could not find user with userId: ${admin.id + 1}`)], errors: [new GraphQLError('Could not find user with given ID')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(`Could not find user with userId: ${admin.id + 1}`) expect(logger.error).toBeCalledWith('Could not find user with given ID', admin.id + 1)
}) })
}) })
@ -1511,13 +1620,13 @@ describe('UserResolver', () => {
mutate({ mutation: deleteUser, variables: { userId: admin.id } }), mutate({ mutation: deleteUser, variables: { userId: admin.id } }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('Moderator can not delete his own account!')], errors: [new GraphQLError('Moderator can not delete his own account')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Moderator can not delete his own account!') expect(logger.error).toBeCalledWith('Moderator can not delete his own account')
}) })
}) })
@ -1545,13 +1654,13 @@ describe('UserResolver', () => {
mutate({ mutation: deleteUser, variables: { userId: user.id } }), mutate({ mutation: deleteUser, variables: { userId: user.id } }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError(`Could not find user with userId: ${user.id}`)], errors: [new GraphQLError('Could not find user with given ID')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(`Could not find user with userId: ${user.id}`) expect(logger.error).toBeCalledWith('Could not find user with given ID', user.id)
}) })
}) })
}) })
@ -1617,13 +1726,13 @@ describe('UserResolver', () => {
mutate({ mutation: unDeleteUser, variables: { userId: admin.id + 1 } }), mutate({ mutation: unDeleteUser, variables: { userId: admin.id + 1 } }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError(`Could not find user with userId: ${admin.id + 1}`)], errors: [new GraphQLError('Could not find user with given ID')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(`Could not find user with userId: ${admin.id + 1}`) expect(logger.error).toBeCalledWith('Could not find user with given ID', admin.id + 1)
}) })
}) })

View File

@ -63,6 +63,7 @@ import { isValidPassword } from '@/password/EncryptorUtils'
import { FULL_CREATION_AVAILABLE } from './const/const' import { FULL_CREATION_AVAILABLE } from './const/const'
import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor' import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor'
import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' import { PasswordEncryptionType } from '../enum/PasswordEncryptionType'
import LogError from '@/server/LogError'
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const sodium = require('sodium-native') const sodium = require('sodium-native')
@ -134,22 +135,19 @@ export class UserResolver {
email = email.trim().toLowerCase() email = email.trim().toLowerCase()
const dbUser = await findUserByEmail(email) const dbUser = await findUserByEmail(email)
if (dbUser.deletedAt) { if (dbUser.deletedAt) {
logger.error('The User was permanently deleted in database.') throw new LogError('This user was permanently deleted. Contact support for questions', dbUser)
throw new Error('This user was permanently deleted. Contact support for questions.')
} }
if (!dbUser.emailContact.emailChecked) { if (!dbUser.emailContact.emailChecked) {
logger.error('The Users email is not validate yet.') throw new LogError('The Users email is not validate yet', dbUser)
throw new Error('User email not validated')
} }
// TODO: at least in test this does not work since `dbUser.password = 0` and `BigInto(0) = 0n`
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 LogError('The User has not set a password yet', dbUser)
} }
if (!verifyPassword(dbUser, password)) { if (!verifyPassword(dbUser, password)) {
logger.error('The User has no valid credentials.') throw new LogError('No user with this credentials', dbUser)
throw new Error('No user with this credentials')
} }
if (dbUser.passwordEncryptionType !== PasswordEncryptionType.GRADIDO_ID) { if (dbUser.passwordEncryptionType !== PasswordEncryptionType.GRADIDO_ID) {
@ -309,30 +307,19 @@ export class UserResolver {
await queryRunner.startTransaction('REPEATABLE READ') await queryRunner.startTransaction('REPEATABLE READ')
try { try {
dbUser = await queryRunner.manager.save(dbUser).catch((error) => { dbUser = await queryRunner.manager.save(dbUser).catch((error) => {
logger.error('Error while saving dbUser', error) throw new LogError('Error while saving dbUser', error)
throw new Error('error saving user')
}) })
let emailContact = newEmailContact(email, dbUser.id) let emailContact = newEmailContact(email, dbUser.id)
emailContact = await queryRunner.manager.save(emailContact).catch((error) => { emailContact = await queryRunner.manager.save(emailContact).catch((error) => {
logger.error('Error while saving emailContact', error) throw new LogError('Error while saving user email contact', error)
throw new Error('error saving email user contact')
}) })
dbUser.emailContact = emailContact dbUser.emailContact = emailContact
dbUser.emailId = emailContact.id dbUser.emailId = emailContact.id
await queryRunner.manager.save(dbUser).catch((error) => { await queryRunner.manager.save(dbUser).catch((error) => {
logger.error('Error while updating dbUser', error) throw new LogError('Error while updating dbUser', error)
throw new Error('error updating user')
}) })
/*
const emailOptIn = newEmailOptIn(dbUser.id)
await queryRunner.manager.save(emailOptIn).catch((error) => {
logger.error('Error while saving emailOptIn', error)
throw new Error('error saving email opt in')
})
*/
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace( const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
/{optin}/g, /{optin}/g,
emailContact.emailVerificationCode.toString(), emailContact.emailVerificationCode.toString(),
@ -358,9 +345,8 @@ export class UserResolver {
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
logger.addContext('user', dbUser.id) logger.addContext('user', dbUser.id)
} catch (e) { } catch (e) {
logger.error(`error during create user with ${e}`)
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
throw e throw new LogError('Error creating user', e)
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }
@ -392,11 +378,9 @@ export class UserResolver {
} }
if (!canEmailResend(user.emailContact.updatedAt || user.emailContact.createdAt)) { if (!canEmailResend(user.emailContact.updatedAt || user.emailContact.createdAt)) {
const errorMessage = `email already sent less than ${printTimeDuration( throw new LogError(
CONFIG.EMAIL_CODE_REQUEST_TIME, `Email already sent less than ${printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)} ago`,
)} ago` )
logger.error(errorMessage)
throw new Error(errorMessage)
} }
user.emailContact.updatedAt = new Date() user.emailContact.updatedAt = new Date()
@ -404,8 +388,7 @@ export class UserResolver {
user.emailContact.emailVerificationCode = random(64) user.emailContact.emailVerificationCode = random(64)
user.emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_RESET_PASSWORD user.emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_RESET_PASSWORD
await user.emailContact.save().catch(() => { await user.emailContact.save().catch(() => {
logger.error('Unable to save email verification code= ' + user.emailContact) throw new LogError('Unable to save email verification code', user.emailContact)
throw new Error('Unable to save email verification code.')
}) })
logger.info(`optInCode for ${email}=${user.emailContact}`) logger.info(`optInCode for ${email}=${user.emailContact}`)
@ -440,34 +423,23 @@ export class UserResolver {
logger.info(`setPassword(${code}, ***)...`) logger.info(`setPassword(${code}, ***)...`)
// Validate Password // Validate Password
if (!isValidPassword(password)) { if (!isValidPassword(password)) {
logger.error('Password entered is lexically invalid') throw new LogError(
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!',
) )
} }
// Load code // load code
/*
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => {
logger.error('Could not login with emailVerificationCode')
throw new Error('Could not login with emailVerificationCode')
})
*/
const userContact = await DbUserContact.findOneOrFail( const userContact = await DbUserContact.findOneOrFail(
{ emailVerificationCode: code }, { emailVerificationCode: code },
{ relations: ['user'] }, { relations: ['user'] },
).catch(() => { ).catch(() => {
logger.error('Could not login with emailVerificationCode') throw new LogError('Could not login with emailVerificationCode')
throw new Error('Could not login with emailVerificationCode')
}) })
logger.debug('userContact loaded...') logger.debug('userContact loaded...')
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) { if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) {
logger.error( throw new LogError(
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`, `Email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
)
throw new Error(
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
) )
} }
logger.debug('EmailVerificationCode is valid...') logger.debug('EmailVerificationCode is valid...')
@ -493,13 +465,11 @@ 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 LogError('Error saving user', error)
throw new Error('error saving user: ' + error)
}) })
// Save userContact // Save userContact
await queryRunner.manager.save(userContact).catch((error) => { await queryRunner.manager.save(userContact).catch((error) => {
logger.error('error saving userContact: ' + error) throw new LogError('Error saving userContact', error)
throw new Error('error saving userContact: ' + error)
}) })
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
@ -510,8 +480,7 @@ export class UserResolver {
eventProtocol.writeEvent(event.setEventActivateAccount(eventActivateAccount)) eventProtocol.writeEvent(event.setEventActivateAccount(eventActivateAccount))
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
logger.error('Error on writing User and UserContact data:' + e) throw new LogError('Error on writing User and User Contact data', e)
throw e
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }
@ -525,7 +494,7 @@ export class UserResolver {
`klicktippSignIn(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`, `klicktippSignIn(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
) )
} catch (e) { } catch (e) {
logger.error('Error subscribe to klicktipp:' + e) logger.error('Error subscribing 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
@ -545,11 +514,8 @@ export class UserResolver {
logger.debug(`found optInCode=${userContact}`) logger.debug(`found optInCode=${userContact}`)
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) { if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) {
logger.error( throw new LogError(
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`, `Email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
)
throw new Error(
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
) )
} }
logger.info(`queryOptIn(${optIn}) successful...`) logger.info(`queryOptIn(${optIn}) successful...`)
@ -584,8 +550,7 @@ export class UserResolver {
if (language) { if (language) {
if (!isLanguage(language)) { if (!isLanguage(language)) {
logger.error(`"${language}" isn't a valid language`) throw new LogError('Given language is not a valid language', language)
throw new Error(`"${language}" isn't a valid language`)
} }
userEntity.language = language userEntity.language = language
i18n.setLocale(language) i18n.setLocale(language)
@ -594,15 +559,13 @@ export class UserResolver {
if (password && passwordNew) { if (password && passwordNew) {
// Validate Password // Validate Password
if (!isValidPassword(passwordNew)) { if (!isValidPassword(passwordNew)) {
logger.error('newPassword does not fullfil the rules') throw new LogError(
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!',
) )
} }
if (!verifyPassword(userEntity, password)) { if (!verifyPassword(userEntity, password)) {
logger.error(`Old password is invalid`) throw new LogError(`Old password is invalid`)
throw new Error(`Old password is invalid`)
} }
// Save new password hash and newly encrypted private key // Save new password hash and newly encrypted private key
@ -625,16 +588,14 @@ export class UserResolver {
try { try {
await queryRunner.manager.save(userEntity).catch((error) => { await queryRunner.manager.save(userEntity).catch((error) => {
logger.error('error saving user: ' + error) throw new LogError('Error saving user', error)
throw new Error('error saving user: ' + error)
}) })
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
logger.debug('writing User data successful...') 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 new LogError('Error on writing updated user data', e)
throw e
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }
@ -761,14 +722,12 @@ export class UserResolver {
const user = await DbUser.findOne({ id: userId }) const user = await DbUser.findOne({ id: userId })
// user exists ? // user exists ?
if (!user) { if (!user) {
logger.error(`Could not find user with userId: ${userId}`) throw new LogError('Could not find user with given ID', userId)
throw new Error(`Could not find user with userId: ${userId}`)
} }
// administrator user changes own role? // administrator user changes own role?
const moderatorUser = getUser(context) const moderatorUser = getUser(context)
if (moderatorUser.id === userId) { if (moderatorUser.id === userId) {
logger.error('Administrator can not change his own role!') throw new LogError('Administrator can not change his own role')
throw new Error('Administrator can not change his own role!')
} }
// change isAdmin // change isAdmin
switch (user.isAdmin) { switch (user.isAdmin) {
@ -776,16 +735,14 @@ export class UserResolver {
if (isAdmin === true) { if (isAdmin === true) {
user.isAdmin = new Date() user.isAdmin = new Date()
} else { } else {
logger.error('User is already a usual user!') throw new LogError('User is already an usual user')
throw new Error('User is already a usual user!')
} }
break break
default: default:
if (isAdmin === false) { if (isAdmin === false) {
user.isAdmin = null user.isAdmin = null
} else { } else {
logger.error('User is already admin!') throw new LogError('User is already admin')
throw new Error('User is already admin!')
} }
break break
} }
@ -803,14 +760,12 @@ export class UserResolver {
const user = await DbUser.findOne({ id: userId }) const user = await DbUser.findOne({ id: userId })
// user exists ? // user exists ?
if (!user) { if (!user) {
logger.error(`Could not find user with userId: ${userId}`) throw new LogError('Could not find user with given ID', userId)
throw new Error(`Could not find user with userId: ${userId}`)
} }
// moderator user disabled own account? // moderator user disabled own account?
const moderatorUser = getUser(context) const moderatorUser = getUser(context)
if (moderatorUser.id === userId) { if (moderatorUser.id === userId) {
logger.error('Moderator can not delete his own account!') throw new LogError('Moderator can not delete his own account')
throw new Error('Moderator can not delete his own account!')
} }
// soft-delete user // soft-delete user
await user.softRemove() await user.softRemove()
@ -823,12 +778,10 @@ export class UserResolver {
async unDeleteUser(@Arg('userId', () => Int) userId: number): Promise<Date | null> { async unDeleteUser(@Arg('userId', () => Int) userId: number): Promise<Date | null> {
const user = await DbUser.findOne({ id: userId }, { withDeleted: true }) const user = await DbUser.findOne({ id: userId }, { withDeleted: true })
if (!user) { if (!user) {
logger.error(`Could not find user with userId: ${userId}`) throw new LogError('Could not find user with given ID', userId)
throw new Error(`Could not find user with userId: ${userId}`)
} }
if (!user.deletedAt) { if (!user.deletedAt) {
logger.error('User is not deleted') throw new LogError('User is not deleted')
throw new Error('User is not deleted')
} }
await user.recover() await user.recover()
return null return null
@ -841,17 +794,14 @@ export class UserResolver {
// const user = await dbUser.findOne({ id: emailContact.userId }) // const user = await dbUser.findOne({ id: emailContact.userId })
const user = await findUserByEmail(email) const user = await findUserByEmail(email)
if (!user) { if (!user) {
logger.error(`Could not find User to emailContact: ${email}`) throw new LogError('Could not find user to given email contact', email)
throw new Error(`Could not find User to emailContact: ${email}`)
} }
if (user.deletedAt) { if (user.deletedAt) {
logger.error(`User with emailContact: ${email} is deleted.`) throw new LogError('User with given email contact is deleted', email)
throw new Error(`User with emailContact: ${email} is deleted.`)
} }
const emailContact = user.emailContact const emailContact = user.emailContact
if (emailContact.deletedAt) { if (emailContact.deletedAt) {
logger.error(`The emailContact: ${email} of this User is deleted.`) throw new LogError('The given email contact for this user is deleted', email)
throw new Error(`The emailContact: ${email} of this User is deleted.`)
} }
emailContact.emailResendCount++ emailContact.emailResendCount++
@ -888,8 +838,7 @@ export async function findUserByEmail(email: string): Promise<DbUser> {
{ email: email }, { email: email },
{ withDeleted: true, relations: ['user'] }, { withDeleted: true, relations: ['user'] },
).catch(() => { ).catch(() => {
logger.error(`UserContact with email=${email} does not exists`) throw new LogError('No user with this credentials', email)
throw new Error('No user with this credentials')
}) })
const dbUser = dbUserContact.user const dbUser = dbUserContact.user
dbUser.emailContact = dbUserContact dbUser.emailContact = dbUserContact
@ -904,31 +853,16 @@ async function checkEmailExists(email: string): Promise<boolean> {
return false return false
} }
/*
const isTimeExpired = (optIn: LoginEmailOptIn, duration: number): boolean => {
const timeElapsed = Date.now() - new Date(optIn.updatedAt).getTime()
// time is given in minutes
return timeElapsed <= duration * 60 * 1000
}
*/
const isTimeExpired = (updatedAt: Date, duration: number): boolean => { const isTimeExpired = (updatedAt: Date, duration: number): boolean => {
const timeElapsed = Date.now() - new Date(updatedAt).getTime() const timeElapsed = Date.now() - new Date(updatedAt).getTime()
// time is given in minutes // time is given in minutes
return timeElapsed <= duration * 60 * 1000 return timeElapsed <= duration * 60 * 1000
} }
/*
const isOptInValid = (optIn: LoginEmailOptIn): boolean => {
return isTimeExpired(optIn, CONFIG.EMAIL_CODE_VALID_TIME)
}
*/
const isEmailVerificationCodeValid = (updatedAt: Date): boolean => { const isEmailVerificationCodeValid = (updatedAt: Date): boolean => {
return isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_VALID_TIME) return isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_VALID_TIME)
} }
/*
const canResendOptIn = (optIn: LoginEmailOptIn): boolean => {
return !isTimeExpired(optIn, CONFIG.EMAIL_CODE_REQUEST_TIME)
}
*/
const canEmailResend = (updatedAt: Date): boolean => { const canEmailResend = (updatedAt: Date): boolean => {
return !isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_REQUEST_TIME) return !isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_REQUEST_TIME)
} }

View File

@ -0,0 +1,26 @@
import { logger } from '@test/testSetup'
import LogError from './LogError'
describe('LogError', () => {
it('logs an Error when created', () => {
/* eslint-disable-next-line no-new */
new LogError('new LogError')
expect(logger.error).toBeCalledWith('new LogError')
})
it('logs an Error including additional data when created', () => {
/* eslint-disable-next-line no-new */
new LogError('new LogError', { some: 'data' })
expect(logger.error).toBeCalledWith('new LogError', { some: 'data' })
})
it('does not contain additional data in Error object when thrown', () => {
try {
throw new LogError('new LogError', { someWeirdValue123: 'arbitraryData456' })
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
} catch (e: any) {
expect(e.stack).not.toMatch(/(someWeirdValue123|arbitraryData456)/i)
}
})
})

View File

@ -0,0 +1,9 @@
import { backendLogger as logger } from './logger'
export default class LogError extends Error {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(msg: string, ...details: any[]) {
super(msg)
logger.error(msg, ...details)
}
}

View File

@ -3,7 +3,7 @@
<b-form <b-form
ref="form" ref="form"
@submit.prevent="submit" @submit.prevent="submit"
class="p-3 bg-white appBoxShadow gradido-border-radius" class="form-style p-3 bg-white appBoxShadow gradido-border-radius"
> >
<label>{{ $t('contribution.selectDate') }}</label> <label>{{ $t('contribution.selectDate') }}</label>
<b-form-datepicker <b-form-datepicker
@ -23,50 +23,70 @@
<template #nav-next-year><span></span></template> <template #nav-next-year><span></span></template>
</b-form-datepicker> </b-form-datepicker>
<input-textarea <div
id="contribution-memo" v-if="(isThisMonth && maxGddThisMonth <= 0) || (!isThisMonth && maxGddLastMonth <= 0)"
v-model="form.memo" class="p-3"
:name="$t('form.message')" >
:label="$t('contribution.activity')" {{ noOpenCreation }}
:placeholder="$t('contribution.yourActivity')" </div>
:rules="{ required: true, min: 5, max: 255 }" <div v-else>
/> <input-textarea
<input-hour id="contribution-memo"
v-model="form.hours" v-model="form.memo"
:name="$t('form.hours')" :name="$t('form.message')"
:label="$t('form.hours')" :label="$t('contribution.activity')"
placeholder="0.25" :placeholder="$t('contribution.yourActivity')"
:rules="{ :rules="{ required: true, min: 5, max: 255 }"
required: true, />
min: 0.25, <input-hour
max: validMaxTime, v-model="form.hours"
gddCreationTime: [0.25, validMaxTime], :name="$t('form.hours')"
}" :label="$t('form.hours')"
:validMaxTime="validMaxTime" placeholder="0.25"
@updateAmount="updateAmount" :rules="{
></input-hour> required: true,
<input-amount min: 0.25,
id="contribution-amount" max: validMaxTime,
v-model="form.amount" gddCreationTime: [0.25, validMaxTime],
:name="$t('form.amount')" }"
:label="$t('form.amount')" :validMaxTime="validMaxTime"
placeholder="20" @updateAmount="updateAmount"
:rules="{ required: true, gddSendAmount: [20, validMaxGDD] }" ></input-hour>
typ="ContributionForm" <input-amount
></input-amount> id="contribution-amount"
v-model="form.amount"
:name="$t('form.amount')"
:label="$t('form.amount')"
placeholder="20"
:rules="{ required: true, gddSendAmount: [20, validMaxGDD] }"
typ="ContributionForm"
></input-amount>
<b-row class="mt-5"> <b-row class="mt-5">
<b-col> <b-col cols="12" lg="6">
<b-button type="reset" variant="secondary" @click="reset" data-test="button-cancel"> <b-button
{{ $t('form.cancel') }} block
</b-button> type="reset"
</b-col> variant="secondary"
<b-col class="text-right"> @click="reset"
<b-button type="submit" variant="gradido" :disabled="disabled" data-test="button-submit"> data-test="button-cancel"
{{ form.id ? $t('form.change') : $t('contribution.submit') }} >
</b-button> {{ $t('form.cancel') }}
</b-col> </b-button>
</b-row> </b-col>
<b-col cols="12" lg="6" class="text-right mt-4 mt-lg-0">
<b-button
block
type="submit"
variant="gradido"
:disabled="disabled"
data-test="button-submit"
>
{{ form.id ? $t('form.change') : $t('contribution.submit') }}
</b-button>
</b-col>
</b-row>
</div>
</b-form> </b-form>
</div> </div>
</template> </template>
@ -133,6 +153,18 @@ export default {
validMaxTime() { validMaxTime() {
return Number(this.validMaxGDD / 20) return Number(this.validMaxGDD / 20)
}, },
noOpenCreation() {
if (this.maxGddThisMonth <= 0 && this.maxGddLastMonth <= 0) {
return this.$t('contribution.noOpenCreation.allMonth')
}
if (this.isThisMonth && this.maxGddThisMonth <= 0) {
return this.$t('contribution.noOpenCreation.thisMonth')
}
if (!this.isThisMonth && this.maxGddLastMonth <= 0) {
return this.$t('contribution.noOpenCreation.lastMonth')
}
return ''
},
}, },
watch: { watch: {
value() { value() {
@ -142,6 +174,9 @@ export default {
} }
</script> </script>
<style> <style>
.form-style {
min-height: 410px;
}
span.errors { span.errors {
color: red; color: red;
} }

View File

@ -57,7 +57,16 @@
"yourContribution": "Dein Beitrag zum Gemeinwohl" "yourContribution": "Dein Beitrag zum Gemeinwohl"
}, },
"lastContribution": "Letzte Beiträge", "lastContribution": "Letzte Beiträge",
"noContributions": {
"allContributions": "Es wurden noch keine Beiträge eingereicht.",
"myContributions": "Du hast noch keine Beiträge eingereicht."
},
"noDateSelected": "Wähle irgendein Datum im Monat", "noDateSelected": "Wähle irgendein Datum im Monat",
"noOpenCreation": {
"allMonth": "Für alle beiden Monate ist dein Schöpfungslimit erreicht. Den Nächsten Monat kannst du wieder 1000 GDD Schöpfen.",
"lastMonth": "Für den ausgewählten Monat ist das Schöpfungslimit erreicht.",
"thisMonth": "Für den aktuellen Monat ist das Schöpfungslimit erreicht."
},
"selectDate": "Wann war dein Beitrag?", "selectDate": "Wann war dein Beitrag?",
"submit": "Einreichen", "submit": "Einreichen",
"submitted": "Der Beitrag wurde eingereicht.", "submitted": "Der Beitrag wurde eingereicht.",

View File

@ -57,7 +57,16 @@
"yourContribution": "Your Contributions to the Common Good" "yourContribution": "Your Contributions to the Common Good"
}, },
"lastContribution": "Last Contributions", "lastContribution": "Last Contributions",
"noContributions": {
"allContributions": "No contributions have been submitted yet.",
"myContributions": "You have not submitted any entries yet."
},
"noDateSelected": "Choose any date in the month", "noDateSelected": "Choose any date in the month",
"noOpenCreation": {
"allMonth": "For all two months your creation limit is reached. The next month you can create 1000 GDD again.",
"lastMonth": "The creation limit is reached for the selected month.",
"thisMonth": "The creation limit has been reached for the current month."
},
"selectDate": "When was your contribution?", "selectDate": "When was your contribution?",
"submit": "Submit", "submit": "Submit",
"submitted": "The contribution was submitted.", "submitted": "The contribution was submitted.",

View File

@ -20,28 +20,38 @@
/> />
</b-tab> </b-tab>
<b-tab no-body> <b-tab no-body>
<contribution-list <div v-if="items.length === 0">
@closeAllOpenCollapse="closeAllOpenCollapse" {{ $t('contribution.noContributions.myContributions') }}
:items="items" </div>
@update-list-contributions="updateListContributions" <div v-else>
@update-contribution-form="updateContributionForm" <contribution-list
@delete-contribution="deleteContribution" @closeAllOpenCollapse="closeAllOpenCollapse"
@update-state="updateState" :items="items"
:contributionCount="contributionCount" @update-list-contributions="updateListContributions"
:showPagination="true" @update-contribution-form="updateContributionForm"
:pageSize="pageSize" @delete-contribution="deleteContribution"
/> @update-state="updateState"
:contributionCount="contributionCount"
:showPagination="true"
:pageSize="pageSize"
/>
</div>
</b-tab> </b-tab>
<b-tab no-body> <b-tab no-body>
<contribution-list <div v-if="itemsAll.length === 0">
:items="itemsAll" {{ $t('contribution.noContributions.allContributions') }}
@update-list-contributions="updateListAllContributions" </div>
@update-contribution-form="updateContributionForm" <div v-else>
:contributionCount="contributionCountAll" <contribution-list
:showPagination="true" :items="itemsAll"
:pageSize="pageSizeAll" @update-list-contributions="updateListAllContributions"
:allContribution="true" @update-contribution-form="updateContributionForm"
/> :contributionCount="contributionCountAll"
:showPagination="true"
:pageSize="pageSizeAll"
:allContribution="true"
/>
</div>
</b-tab> </b-tab>
</b-tabs> </b-tabs>
</div> </div>