Merge branch 'master' into dissolve_admin_resolver

# Conflicts:
#	backend/src/graphql/resolver/AdminResolver.ts
This commit is contained in:
Ulf Gebhardt 2022-12-13 16:20:30 +01:00
commit 330f85e04e
Signed by: ulfgebhardt
GPG Key ID: DA6B843E748679C9
4 changed files with 175 additions and 134 deletions

View File

@ -0,0 +1,126 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { testEnvironment, cleanDB } from '@test/helpers'
import { User as DbUser } from '@entity/User'
import { createUser, setPassword, forgotPassword } from '@/seeds/graphql/mutations'
import { queryOptIn } from '@/seeds/graphql/queries'
import CONFIG from '@/config'
import { GraphQLError } from 'graphql'
let mutate: any, query: any, con: any
let testEnv: any
CONFIG.EMAIL_CODE_VALID_TIME = 1440
CONFIG.EMAIL_CODE_REQUEST_TIME = 10
CONFIG.EMAIL = false
beforeAll(async () => {
testEnv = await testEnvironment()
mutate = testEnv.mutate
query = testEnv.query
con = testEnv.con
await cleanDB()
})
afterAll(async () => {
await cleanDB()
await con.close()
})
describe('EmailOptinCodes', () => {
let optinCode: string
beforeAll(async () => {
const variables = {
email: 'peter@lustig.de',
firstName: 'Peter',
lastName: 'Lustig',
language: 'de',
}
const {
data: { createUser: user },
} = await mutate({ mutation: createUser, variables })
const dbObject = await DbUser.findOneOrFail({
where: { id: user.id },
relations: ['emailContact'],
})
optinCode = dbObject.emailContact.emailVerificationCode.toString()
})
describe('queryOptIn', () => {
it('has a valid optin code', async () => {
await expect(
query({ query: queryOptIn, variables: { optIn: optinCode } }),
).resolves.toMatchObject({
data: {
queryOptIn: true,
},
errors: undefined,
})
})
describe('run time forward until code must be expired', () => {
beforeAll(() => {
jest.useFakeTimers()
setTimeout(jest.fn(), CONFIG.EMAIL_CODE_VALID_TIME * 60 * 1000)
jest.runAllTimers()
})
afterAll(() => {
jest.useRealTimers()
})
it('throws an error', async () => {
await expect(
query({ query: queryOptIn, variables: { optIn: optinCode } }),
).resolves.toMatchObject({
data: null,
errors: [new GraphQLError('email was sent more than 24 hours ago')],
})
})
it('does not allow to set password', async () => {
await expect(
mutate({ mutation: setPassword, variables: { code: optinCode, password: 'Aa12345_' } }),
).resolves.toMatchObject({
data: null,
errors: [new GraphQLError('email was sent more than 24 hours ago')],
})
})
})
})
describe('forgotPassword', () => {
it('throws an error', async () => {
await expect(
mutate({ mutation: forgotPassword, variables: { email: 'peter@lustig.de' } }),
).resolves.toMatchObject({
data: null,
errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')],
})
})
describe('run time forward until code can be resent', () => {
beforeAll(() => {
jest.useFakeTimers()
setTimeout(jest.fn(), CONFIG.EMAIL_CODE_REQUEST_TIME * 60 * 1000)
jest.runAllTimers()
})
afterAll(() => {
jest.useRealTimers()
})
it('cann send email again', async () => {
await expect(
mutate({ mutation: forgotPassword, variables: { email: 'peter@lustig.de' } }),
).resolves.toMatchObject({
data: {
forgotPassword: true,
},
errors: undefined,
})
})
})
})
})

View File

@ -29,7 +29,6 @@ import {
sendAccountMultiRegistrationEmail,
sendResetPasswordEmail,
} from '@/emails/sendEmailVariants'
import { activationLink } from './UserResolver'
import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
import { transactionLinkFactory } from '@/seeds/factory/transactionLink'
import { ContributionLink } from '@model/ContributionLink'
@ -815,12 +814,8 @@ describe('UserResolver', () => {
})
describe('user exists in DB', () => {
let emailContact: UserContact
beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg)
// await resetEntity(LoginEmailOptIn)
emailContact = await UserContact.findOneOrFail(variables)
})
afterAll(async () => {
@ -829,7 +824,7 @@ describe('UserResolver', () => {
})
describe('duration not expired', () => {
it('returns true', async () => {
it('throws an error', async () => {
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
expect.objectContaining({
errors: [
@ -855,19 +850,19 @@ describe('UserResolver', () => {
}),
)
})
})
it('sends reset password email', () => {
expect(sendResetPasswordEmail).toBeCalledWith({
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
language: 'de',
resetLink: activationLink(emailContact.emailVerificationCode),
timeDurationObject: expect.objectContaining({
hours: expect.any(Number),
minutes: expect.any(Number),
}),
it('sends reset password email', () => {
expect(sendResetPasswordEmail).toBeCalledWith({
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
language: 'de',
resetLink: expect.any(String),
timeDurationObject: expect.objectContaining({
hours: expect.any(Number),
minutes: expect.any(Number),
}),
})
})
})

View File

@ -126,16 +126,6 @@ const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => {
return [pubKey, privKey]
}
/*
const getEmailHash = (email: string): Buffer => {
logger.trace('getEmailHash...')
const emailHash = Buffer.alloc(sodium.crypto_generichash_BYTES)
sodium.crypto_generichash(emailHash, Buffer.from(email))
logger.debug(`getEmailHash...successful: ${emailHash}`)
return emailHash
}
*/
const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => {
logger.trace('SecretKeyCryptographyEncrypt...')
const encrypted = Buffer.alloc(message.length + sodium.crypto_secretbox_MACBYTES)
@ -171,91 +161,6 @@ const newEmailContact = (email: string, userId: number): DbUserContact => {
logger.debug(`newEmailContact...successful: ${emailContact}`)
return emailContact
}
/*
const newEmailOptIn = (userId: number): LoginEmailOptIn => {
logger.trace('newEmailOptIn...')
const emailOptIn = new LoginEmailOptIn()
emailOptIn.verificationCode = random(64)
emailOptIn.userId = userId
emailOptIn.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER
logger.debug(`newEmailOptIn...successful: ${emailOptIn}`)
return emailOptIn
}
*/
/*
// needed by AdminResolver
// checks if given code exists and can be resent
// if optIn does not exits, it is created
export const checkOptInCode = async (
optInCode: LoginEmailOptIn | undefined,
user: DbUser,
optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER,
): Promise<LoginEmailOptIn> => {
logger.info(`checkOptInCode... ${optInCode}`)
if (optInCode) {
if (!canResendOptIn(optInCode)) {
logger.error(
`email already sent less than ${printTimeDuration(
CONFIG.EMAIL_CODE_REQUEST_TIME,
)} minutes ago`,
)
throw new Error(
`email already sent less than ${printTimeDuration(
CONFIG.EMAIL_CODE_REQUEST_TIME,
)} minutes ago`,
)
}
optInCode.updatedAt = new Date()
optInCode.resendCount++
} else {
logger.trace('create new OptIn for userId=' + user.id)
optInCode = newEmailOptIn(user.id)
}
if (user.emailChecked) {
optInCode.emailOptInTypeId = optInType
}
await LoginEmailOptIn.save(optInCode).catch(() => {
logger.error('Unable to save optin code= ' + optInCode)
throw new Error('Unable to save optin code.')
})
logger.debug(`checkOptInCode...successful: ${optInCode} for userid=${user.id}`)
return optInCode
}
*/
export const checkEmailVerificationCode = async (
emailContact: DbUserContact,
optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER,
): Promise<DbUserContact> => {
logger.info(`checkEmailVerificationCode... ${emailContact}`)
if (emailContact.updatedAt) {
if (!canEmailResend(emailContact.updatedAt)) {
logger.error(
`email already sent less than ${printTimeDuration(
CONFIG.EMAIL_CODE_REQUEST_TIME,
)} minutes ago`,
)
throw new Error(
`email already sent less than ${printTimeDuration(
CONFIG.EMAIL_CODE_REQUEST_TIME,
)} minutes ago`,
)
}
emailContact.updatedAt = new Date()
emailContact.emailResendCount++
} else {
logger.trace('create new EmailVerificationCode for userId=' + emailContact.userId)
emailContact.emailChecked = false
emailContact.emailVerificationCode = random(64)
}
emailContact.emailOptInTypeId = optInType
await DbUserContact.save(emailContact).catch(() => {
logger.error('Unable to save email verification code= ' + emailContact)
throw new Error('Unable to save email verification code.')
})
logger.debug(`checkEmailVerificationCode...successful: ${emailContact}`)
return emailContact
}
export const activationLink = (verificationCode: BigInt): string => {
logger.debug(`activationLink(${verificationCode})...`)
@ -366,6 +271,7 @@ export class UserResolver {
@Authorized([RIGHTS.LOGOUT])
@Mutation(() => String)
async logout(): Promise<boolean> {
// TODO: Event still missing here!!
// TODO: We dont need this anymore, but might need this in the future in oder to invalidate a valid JWT-Token.
// Furthermore this hook can be useful for tracking user behaviour (did he logout or not? Warn him if he didn't on next login)
// The functionality is fully client side - the client just needs to delete his token with the current implementation.
@ -579,32 +485,45 @@ export class UserResolver {
return true
}
// can be both types: REGISTER and RESET_PASSWORD
// let optInCode = await LoginEmailOptIn.findOne({
// userId: user.id,
// })
// let optInCode = user.emailContact.emailVerificationCode
const dbUserContact = await checkEmailVerificationCode(
user.emailContact,
OptInType.EMAIL_OPT_IN_RESET_PASSWORD,
)
if (!canEmailResend(user.emailContact.updatedAt || user.emailContact.createdAt)) {
logger.error(
`email already sent less than ${printTimeDuration(
CONFIG.EMAIL_CODE_REQUEST_TIME,
)} minutes ago`,
)
throw new Error(
`email already sent less than ${printTimeDuration(
CONFIG.EMAIL_CODE_REQUEST_TIME,
)} minutes ago`,
)
}
// optInCode = await checkOptInCode(optInCode, user, OptInType.EMAIL_OPT_IN_RESET_PASSWORD)
logger.info(`optInCode for ${email}=${dbUserContact}`)
user.emailContact.updatedAt = new Date()
user.emailContact.emailResendCount++
user.emailContact.emailVerificationCode = random(64)
user.emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_RESET_PASSWORD
await user.emailContact.save().catch(() => {
logger.error('Unable to save email verification code= ' + user.emailContact)
throw new Error('Unable to save email verification code.')
})
logger.info(`optInCode for ${email}=${user.emailContact}`)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const emailSent = await sendResetPasswordEmail({
firstName: user.firstName,
lastName: user.lastName,
email,
language: user.language,
resetLink: activationLink(dbUserContact.emailVerificationCode),
resetLink: activationLink(user.emailContact.emailVerificationCode),
timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME),
})
/* uncomment this, when you need the activation link on the console */
// In case EMails are disabled log the activation link for the user
if (!emailSent) {
logger.debug(`Reset password link: ${activationLink(dbUserContact.emailVerificationCode)}`)
logger.debug(
`Reset password link: ${activationLink(user.emailContact.emailVerificationCode)}`,
)
}
logger.info(`forgotPassword(${email}) successful...`)
@ -642,7 +561,7 @@ export class UserResolver {
})
logger.debug('userContact loaded...')
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
if (!isEmailVerificationCodeValid(userContact.updatedAt)) {
if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) {
logger.error(
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
)
@ -746,7 +665,7 @@ export class UserResolver {
const userContact = await DbUserContact.findOneOrFail({ emailVerificationCode: optIn })
logger.debug(`found optInCode=${userContact}`)
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
if (!isEmailVerificationCodeValid(userContact.updatedAt)) {
if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) {
logger.error(
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
)
@ -1055,6 +974,9 @@ export class UserResolver {
throw new Error(`The emailContact: ${email} of this User is deleted.`)
}
emailContact.emailResendCount++
await emailContact.save()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const emailSent = await sendAccountActivationEmail({
firstName: user.firstName,
@ -1119,10 +1041,7 @@ const isOptInValid = (optIn: LoginEmailOptIn): boolean => {
return isTimeExpired(optIn, CONFIG.EMAIL_CODE_VALID_TIME)
}
*/
const isEmailVerificationCodeValid = (updatedAt: Date | null): boolean => {
if (updatedAt == null) {
return true
}
const isEmailVerificationCodeValid = (updatedAt: Date): boolean => {
return isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_VALID_TIME)
}
/*

View File

@ -5,6 +5,7 @@ import { createTestClient } from 'apollo-server-testing'
import createServer from '../src/server/createServer'
import { initialize } from '@dbTools/helpers'
import { entities } from '@entity/index'
import { i18n, logger } from './testSetup'
export const headerPushMock = jest.fn((t) => {
context.token = t.value
@ -26,8 +27,8 @@ export const cleanDB = async () => {
}
}
export const testEnvironment = async (logger?: any, localization?: any) => {
const server = await createServer(context, logger, localization)
export const testEnvironment = async (testLogger: any = logger, testI18n: any = i18n) => {
const server = await createServer(context, testLogger, testI18n)
const con = server.con
const testClient = createTestClient(server.apollo)
const mutate = testClient.mutate