mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into dissolve_admin_resolver
# Conflicts: # backend/src/graphql/resolver/AdminResolver.ts
This commit is contained in:
commit
330f85e04e
126
backend/src/graphql/resolver/EmailOptinCodes.test.ts
Normal file
126
backend/src/graphql/resolver/EmailOptinCodes.test.ts
Normal 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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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),
|
||||
}),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
/*
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user