From 2faad50919eedf01ae3a42360ace60e80d1608c1 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 23 Mar 2022 13:33:07 +0100 Subject: [PATCH 01/41] set email code valid time to 24h --- backend/.env.dist | 2 +- backend/src/config/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/.env.dist b/backend/.env.dist index 3c93f1576..bbea28989 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -40,7 +40,7 @@ EMAIL_SMTP_URL=gmail.com EMAIL_SMTP_PORT=587 EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{code} EMAIL_LINK_SETPASSWORD=http://localhost/reset/{code} -EMAIL_CODE_VALID_TIME=10 +EMAIL_CODE_VALID_TIME=1440 # Webhook WEBHOOK_ELOPAGE_SECRET=secret \ No newline at end of file diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 754bfbf08..81d72478a 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -66,8 +66,8 @@ const email = { EMAIL_LINK_SETPASSWORD: process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{code}', EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME - ? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 10 - : 10, + ? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 1440 + : 1440, } const webhook = { From 662285161cb80f33dea50765892b4d63db62486b Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 23 Mar 2022 14:28:24 +0100 Subject: [PATCH 02/41] time span for email code valid time --- backend/src/graphql/resolver/UserResolver.ts | 26 +++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index a56b67945..9b4fa6fa1 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -157,7 +157,7 @@ const createEmailOptIn = async ( }) if (emailOptIn) { if (isOptInCodeValid(emailOptIn)) { - throw new Error(`email already sent less than $(CONFIG.EMAIL_CODE_VALID_TIME} minutes ago`) + throw new Error(`email already sent less than ${printEmailCodeValidTime()} ago`) } emailOptIn.updatedAt = new Date() emailOptIn.resendCount++ @@ -184,7 +184,7 @@ const getOptInCode = async (loginUserId: number): Promise => { // Check for `CONFIG.EMAIL_CODE_VALID_TIME` minute delay if (optInCode) { if (isOptInCodeValid(optInCode)) { - throw new Error(`email already sent less than $(CONFIG.EMAIL_CODE_VALID_TIME} minutes ago`) + throw new Error(`email already sent less than $(printEmailCodeValidTime()} minutes ago`) } optInCode.updatedAt = new Date() optInCode.resendCount++ @@ -486,7 +486,7 @@ export class UserResolver { // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes if (!isOptInCodeValid(optInCode)) { - throw new Error(`email already more than $(CONFIG.EMAIL_CODE_VALID_TIME} minutes ago`) + throw new Error(`email was sent more than ${printEmailCodeValidTime()} ago`) } // load user @@ -565,7 +565,7 @@ export class UserResolver { const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: optIn }) // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes if (!isOptInCodeValid(optInCode)) { - throw new Error(`email was sent more than $(CONFIG.EMAIL_CODE_VALID_TIME} minutes ago`) + throw new Error(`email was sent more than $(printEmailCodeValidTime()} ago`) } return true } @@ -671,7 +671,25 @@ export class UserResolver { return hasElopageBuys(userEntity.email) } } + function isOptInCodeValid(optInCode: LoginEmailOptIn) { const timeElapsed = Date.now() - new Date(optInCode.updatedAt).getTime() return timeElapsed <= CONFIG.EMAIL_CODE_VALID_TIME * 60 * 1000 } + +const emailCodeValidTime = (): { hours?: number; minutes: number } => { + if (CONFIG.EMAIL_CODE_VALID_TIME > 60) { + return { + hours: Math.floor(CONFIG.EMAIL_CODE_VALID_TIME / 60), + minutes: CONFIG.EMAIL_CODE_VALID_TIME % 60, + } + } + return { minutes: CONFIG.EMAIL_CODE_VALID_TIME } +} + +const printEmailCodeValidTime = (): string => { + const time = emailCodeValidTime() + const result = time.minutes > 0 ? `${time.minutes} minutes` : '' + if (time.hours) return `${time.hours} hours` + result !== '' ? ` and ${result}` : '' + return result +} From ca0d97c47cde3152aa5929658242f87619c9a686 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 23 Mar 2022 16:03:52 +0100 Subject: [PATCH 03/41] test printEmailCodeValidTime --- .../src/graphql/resolver/UserResolver.test.ts | 18 ++++++++++++++++++ backend/src/graphql/resolver/UserResolver.ts | 4 ++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index f873fd694..9d78ecb6e 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -11,6 +11,7 @@ import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { User } from '@entity/User' import CONFIG from '@/config' import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' +import { printEmailCodeValidTime } from './UserResolver' // import { klicktippSignIn } from '@/apis/KlicktippController' @@ -412,3 +413,20 @@ describe('UserResolver', () => { }) }) }) + +describe('printEmailCodeValidTime', () => { + it('works with 10 minutes', () => { + CONFIG.EMAIL_CODE_VALID_TIME = 10 + expect(printEmailCodeValidTime()).toBe('10 minutes') + }) + + it('works with 1440 minutes', () => { + CONFIG.EMAIL_CODE_VALID_TIME = 1440 + expect(printEmailCodeValidTime()).toBe('24 hours') + }) + + it('works with 1410 minutes', () => { + CONFIG.EMAIL_CODE_VALID_TIME = 1410 + expect(printEmailCodeValidTime()).toBe('23 hours and 30 minutes') + }) +}) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 9b4fa6fa1..e0aa06a7c 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -687,9 +687,9 @@ const emailCodeValidTime = (): { hours?: number; minutes: number } => { return { minutes: CONFIG.EMAIL_CODE_VALID_TIME } } -const printEmailCodeValidTime = (): string => { +export const printEmailCodeValidTime = (): string => { const time = emailCodeValidTime() const result = time.minutes > 0 ? `${time.minutes} minutes` : '' - if (time.hours) return `${time.hours} hours` + result !== '' ? ` and ${result}` : '' + if (time.hours) return `${time.hours} hours` + (result !== '' ? ` and ${result}` : '') return result } From 40520c6718cbc41ac10b9aa6510f007e67c0cad1 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 23 Mar 2022 16:11:19 +0100 Subject: [PATCH 04/41] regexp to check for error message --- frontend/src/pages/ResetPassword.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/ResetPassword.vue b/frontend/src/pages/ResetPassword.vue index 7532953ed..bf8664cef 100644 --- a/frontend/src/pages/ResetPassword.vue +++ b/frontend/src/pages/ResetPassword.vue @@ -104,7 +104,11 @@ export default { }) .catch((error) => { this.toastError(error.message) - if (error.message.includes('Code is older than 10 minutes')) + if ( + error.message.match( + /email was sent more than ([0-9]+ hours)?( and)?([0-9]+ minutes)? ago/, + ) + ) this.$router.push('/forgot-password/resetPassword') }) }, From b0300e5a084d004e64e72901b8e947245c24b5ba Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 23 Mar 2022 16:16:04 +0100 Subject: [PATCH 05/41] test regexp to catch error message --- frontend/src/pages/ResetPassword.spec.js | 8 ++++++-- frontend/src/pages/ResetPassword.vue | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/ResetPassword.spec.js b/frontend/src/pages/ResetPassword.spec.js index f5d672c99..8f2d63791 100644 --- a/frontend/src/pages/ResetPassword.spec.js +++ b/frontend/src/pages/ResetPassword.spec.js @@ -149,13 +149,17 @@ describe('ResetPassword', () => { describe('server response with error code > 10min', () => { beforeEach(async () => { jest.clearAllMocks() - apolloMutationMock.mockRejectedValue({ message: '...Code is older than 10 minutes' }) + apolloMutationMock.mockRejectedValue({ + message: '...email was sent more than 23 hours and 10 minutes ago', + }) await wrapper.find('form').trigger('submit') await flushPromises() }) it('toasts an error message', () => { - expect(toastErrorSpy).toHaveBeenCalledWith('...Code is older than 10 minutes') + expect(toastErrorSpy).toHaveBeenCalledWith( + '...email was sent more than 23 hours and 10 minutes ago', + ) }) it('router pushes to /forgot-password/resetPassword', () => { diff --git a/frontend/src/pages/ResetPassword.vue b/frontend/src/pages/ResetPassword.vue index bf8664cef..02773e5e4 100644 --- a/frontend/src/pages/ResetPassword.vue +++ b/frontend/src/pages/ResetPassword.vue @@ -106,7 +106,7 @@ export default { this.toastError(error.message) if ( error.message.match( - /email was sent more than ([0-9]+ hours)?( and)?([0-9]+ minutes)? ago/, + /email was sent more than ([0-9]+ hours)?( and )?([0-9]+ minutes)? ago/, ) ) this.$router.push('/forgot-password/resetPassword') From 3d613e72f530876814896797781b6738a0cbbd74 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 23 Mar 2022 16:48:35 +0100 Subject: [PATCH 06/41] add link to forgot-password --- backend/.env.dist | 1 + backend/src/config/index.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/backend/.env.dist b/backend/.env.dist index bbea28989..ab0527836 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -40,6 +40,7 @@ EMAIL_SMTP_URL=gmail.com EMAIL_SMTP_PORT=587 EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{code} EMAIL_LINK_SETPASSWORD=http://localhost/reset/{code} +EMAIL_LINK_FORGOTPASSWORD=http://localhost/forgot-password EMAIL_CODE_VALID_TIME=1440 # Webhook diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 81d72478a..23385fe02 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -65,6 +65,8 @@ const email = { process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{code}', EMAIL_LINK_SETPASSWORD: process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{code}', + EMAIL_LINK_FORGOTPASSWORD: + process.env.EMAIL_LINK_FORGOTPASSWORD || 'http://localhost/forgot-password', EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME ? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 1440 : 1440, From d1644ad2aeb7eacc39f91b2a56ad5142ec86e794 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 23 Mar 2022 16:49:30 +0100 Subject: [PATCH 07/41] add information about link duration and how to get a new link in email --- backend/src/graphql/resolver/UserResolver.ts | 2 ++ .../src/mailer/sendAccountActivationEmail.ts | 4 +++- backend/src/mailer/text/accountActivation.ts | 17 ++++++++++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index e0aa06a7c..6f7f82e02 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -370,6 +370,7 @@ export class UserResolver { firstName, lastName, email, + duration: printEmailCodeValidTime(), }) /* uncomment this, when you need the activation link on the console @@ -414,6 +415,7 @@ export class UserResolver { firstName: user.firstName, lastName: user.lastName, email, + duration: printEmailCodeValidTime(), }) /* uncomment this, when you need the activation link on the console diff --git a/backend/src/mailer/sendAccountActivationEmail.ts b/backend/src/mailer/sendAccountActivationEmail.ts index 05c3104cb..335f80a82 100644 --- a/backend/src/mailer/sendAccountActivationEmail.ts +++ b/backend/src/mailer/sendAccountActivationEmail.ts @@ -1,15 +1,17 @@ import { sendEMail } from './sendEMail' import { accountActivation } from './text/accountActivation' +import CONFIG from '@/config' export const sendAccountActivationEmail = (data: { link: string firstName: string lastName: string email: string + duration: string }): Promise => { return sendEMail({ to: `${data.firstName} ${data.lastName} <${data.email}>`, subject: accountActivation.de.subject, - text: accountActivation.de.text(data), + text: accountActivation.de.text({ ...data, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD }), }) } diff --git a/backend/src/mailer/text/accountActivation.ts b/backend/src/mailer/text/accountActivation.ts index bf5d1a2e9..ea33c04d0 100644 --- a/backend/src/mailer/text/accountActivation.ts +++ b/backend/src/mailer/text/accountActivation.ts @@ -1,7 +1,14 @@ export const accountActivation = { de: { subject: 'Gradido: E-Mail Überprüfung', - text: (data: { link: string; firstName: string; lastName: string; email: string }): string => + text: (data: { + link: string + firstName: string + lastName: string + email: string + duration: string + resendLink: string + }): string => `Hallo ${data.firstName} ${data.lastName}, Deine E-Mail-Adresse wurde soeben bei Gradido registriert. @@ -9,6 +16,14 @@ Deine E-Mail-Adresse wurde soeben bei Gradido registriert. Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren: ${data.link} oder kopiere den obigen Link in dein Browserfenster. +Der Link hat eine Gültigkeit von ${data.duration + .replace('hours', 'Stunden') + .replace('minutes', 'Minuten') + .replace( + ' and ', + ' und ', + )}. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst: +${data.resendLink} Mit freundlichen Grüßen, dein Gradido-Team`, From f1686523df099d7f846bed2fb4e0bf2db79d8726 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 23 Mar 2022 16:57:04 +0100 Subject: [PATCH 08/41] add link information to reset password email --- backend/src/graphql/resolver/UserResolver.ts | 5 +++-- backend/src/mailer/sendResetPasswordEmail.ts | 4 +++- backend/src/mailer/text/accountActivation.ts | 1 + backend/src/mailer/text/resetPassword.ts | 18 +++++++++++++++++- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 6f7f82e02..2d2355f17 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -15,7 +15,7 @@ import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddle import { UserSettingRepository } from '@repository/UserSettingRepository' import { Setting } from '@enum/Setting' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' -import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail' +import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail' import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' import { klicktippSignIn } from '@/apis/KlicktippController' import { RIGHTS } from '@/auth/RIGHTS' @@ -450,11 +450,12 @@ export class UserResolver { ) // eslint-disable-next-line @typescript-eslint/no-unused-vars - const emailSent = await sendResetPasswordEmail({ + const emailSent = await sendResetPasswordEmailMailer({ link, firstName: user.firstName, lastName: user.lastName, email, + duration: printEmailCodeValidTime(), }) /* uncomment this, when you need the activation link on the console diff --git a/backend/src/mailer/sendResetPasswordEmail.ts b/backend/src/mailer/sendResetPasswordEmail.ts index c9f5b23e9..d9770f940 100644 --- a/backend/src/mailer/sendResetPasswordEmail.ts +++ b/backend/src/mailer/sendResetPasswordEmail.ts @@ -1,15 +1,17 @@ import { sendEMail } from './sendEMail' import { resetPassword } from './text/resetPassword' +import CONFIG from '@/config' export const sendResetPasswordEmail = (data: { link: string firstName: string lastName: string email: string + duration: string }): Promise => { return sendEMail({ to: `${data.firstName} ${data.lastName} <${data.email}>`, subject: resetPassword.de.subject, - text: resetPassword.de.text(data), + text: resetPassword.de.text({ ...data, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD }), }) } diff --git a/backend/src/mailer/text/accountActivation.ts b/backend/src/mailer/text/accountActivation.ts index ea33c04d0..2755c4c0a 100644 --- a/backend/src/mailer/text/accountActivation.ts +++ b/backend/src/mailer/text/accountActivation.ts @@ -16,6 +16,7 @@ Deine E-Mail-Adresse wurde soeben bei Gradido registriert. Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren: ${data.link} oder kopiere den obigen Link in dein Browserfenster. + Der Link hat eine Gültigkeit von ${data.duration .replace('hours', 'Stunden') .replace('minutes', 'Minuten') diff --git a/backend/src/mailer/text/resetPassword.ts b/backend/src/mailer/text/resetPassword.ts index 58b13cbcd..ff660f76e 100644 --- a/backend/src/mailer/text/resetPassword.ts +++ b/backend/src/mailer/text/resetPassword.ts @@ -1,13 +1,29 @@ export const resetPassword = { de: { subject: 'Gradido: Passwort zurücksetzen', - text: (data: { link: string; firstName: string; lastName: string; email: string }): string => + text: (data: { + link: string + firstName: string + lastName: string + email: string + duration: string + resendLink: string + }): string => `Hallo ${data.firstName} ${data.lastName}, Du oder jemand anderes hat für dieses Konto ein Zurücksetzen des Passworts angefordert. Wenn du es warst, klicke bitte auf den Link: ${data.link} oder kopiere den obigen Link in Dein Browserfenster. +Der Link hat eine Gültigkeit von ${data.duration + .replace('hours', 'Stunden') + .replace('minutes', 'Minuten') + .replace( + ' and ', + ' und ', + )}. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst: +${data.resendLink} + Mit freundlichen Grüßen, dein Gradido-Team`, }, From cc0efb22040f25ddcb736ef88798c6bfff82669c Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 23 Mar 2022 17:02:53 +0100 Subject: [PATCH 09/41] test link duration in mails --- backend/src/mailer/sendAccountActivationEmail.test.ts | 5 ++++- backend/src/mailer/sendResetPasswordEmail.test.ts | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/backend/src/mailer/sendAccountActivationEmail.test.ts b/backend/src/mailer/sendAccountActivationEmail.test.ts index c53fc0994..08ddae166 100644 --- a/backend/src/mailer/sendAccountActivationEmail.test.ts +++ b/backend/src/mailer/sendAccountActivationEmail.test.ts @@ -15,6 +15,7 @@ describe('sendAccountActivationEmail', () => { firstName: 'Peter', lastName: 'Lustig', email: 'peter@lustig.de', + duration: '23 hours and 30 minutes', }) }) @@ -23,7 +24,9 @@ describe('sendAccountActivationEmail', () => { to: `Peter Lustig `, subject: 'Gradido: E-Mail Überprüfung', text: - expect.stringContaining('Hallo Peter Lustig') && expect.stringContaining('activationLink'), + expect.stringContaining('Hallo Peter Lustig') && + expect.stringContaining('activationLink') && + expect.stringContaining('23 Stunden und 30 Minuten'), }) }) }) diff --git a/backend/src/mailer/sendResetPasswordEmail.test.ts b/backend/src/mailer/sendResetPasswordEmail.test.ts index 4bc8ceba0..94f69cf8b 100644 --- a/backend/src/mailer/sendResetPasswordEmail.test.ts +++ b/backend/src/mailer/sendResetPasswordEmail.test.ts @@ -15,6 +15,7 @@ describe('sendResetPasswordEmail', () => { firstName: 'Peter', lastName: 'Lustig', email: 'peter@lustig.de', + duration: '23 hours and 30 minutes', }) }) @@ -22,7 +23,10 @@ describe('sendResetPasswordEmail', () => { expect(sendEMail).toBeCalledWith({ to: `Peter Lustig `, subject: 'Gradido: Passwort zurücksetzen', - text: expect.stringContaining('Hallo Peter Lustig') && expect.stringContaining('resetLink'), + text: + expect.stringContaining('Hallo Peter Lustig') && + expect.stringContaining('resetLink') && + expect.stringContaining('23 Stunden und 30 Minuten'), }) }) }) From 174e1ab597baf077532e73b79abe7a50a421e457 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 23 Mar 2022 17:12:09 +0100 Subject: [PATCH 10/41] fix test --- backend/src/graphql/resolver/UserResolver.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 9d78ecb6e..93c8016e0 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -131,6 +131,7 @@ describe('UserResolver', () => { firstName: 'Peter', lastName: 'Lustig', email: 'peter@lustig.de', + duration: expect.any(String), }) }) }) From 8277773006aa8f8d4f732e07fc0183f3e6321809 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 23 Mar 2022 18:13:54 +0100 Subject: [PATCH 11/41] add gdtSum query --- backend/src/auth/RIGHTS.ts | 1 + backend/src/auth/ROLES.ts | 1 + backend/src/graphql/resolver/GdtResolver.ts | 21 +++++++++++++++++++-- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index f40088779..57401d361 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -24,6 +24,7 @@ export enum RIGHTS { QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK', REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK', LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS', + GDT_SUM = 'GDT_SUM', // Admin SEARCH_USERS = 'SEARCH_USERS', CREATE_PENDING_CREATION = 'CREATE_PENDING_CREATION', diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts index 82c689848..5cb8f833a 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -22,6 +22,7 @@ export const ROLE_USER = new Role('user', [ RIGHTS.DELETE_TRANSACTION_LINK, RIGHTS.REDEEM_TRANSACTION_LINK, RIGHTS.LIST_TRANSACTION_LINKS, + RIGHTS.GDT_SUM, ]) export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights diff --git a/backend/src/graphql/resolver/GdtResolver.ts b/backend/src/graphql/resolver/GdtResolver.ts index 26ae9b210..302b17a4a 100644 --- a/backend/src/graphql/resolver/GdtResolver.ts +++ b/backend/src/graphql/resolver/GdtResolver.ts @@ -13,13 +13,11 @@ import { RIGHTS } from '@/auth/RIGHTS' export class GdtResolver { @Authorized([RIGHTS.LIST_GDT_ENTRIES]) @Query(() => GdtEntryList) - // eslint-disable-next-line @typescript-eslint/no-explicit-any async listGDTEntries( @Args() { currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated, @Ctx() context: any, ): Promise { - // load user const userEntity = context.user try { @@ -35,6 +33,25 @@ export class GdtResolver { } } + @Authorized([RIGHTS.GDT_SUM]) + @Query(() => Number | null) + async gdtSum(@Ctx() context: any): Promise { + const { user } = context + try { + const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, { + email: user.email, + }) + if (!resultGDTSum.success) { + throw new Error('Call not successful') + } + return Number(resultGDTSum.data.sum) || 0 + } catch (err: any) { + // eslint-disable-next-line no-console + console.log('Could not query GDT Server', err) + return null + } + } + @Authorized([RIGHTS.EXIST_PID]) @Query(() => Number) // eslint-disable-next-line @typescript-eslint/no-explicit-any From 243058ee5a6cddbd1e474b5939f510509dd967f8 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 23 Mar 2022 18:18:07 +0100 Subject: [PATCH 12/41] get GDT sum via GDT resolver --- .../graphql/resolver/TransactionResolver.ts | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 03640817f..3164eaf8e 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -6,7 +6,6 @@ import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql' import { getCustomRepository, getConnection } from '@dbTools/typeorm' -import CONFIG from '@/config' import { sendTransactionReceivedEmail } from '@/mailer/sendTransactionReceivedEmail' import { Transaction } from '@model/Transaction' @@ -24,7 +23,6 @@ import { User as dbUser } from '@entity/User' import { Transaction as dbTransaction } from '@entity/Transaction' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' -import { apiPost } from '@/apis/HttpRequest' import { TransactionTypeId } from '@enum/TransactionTypeId' import { calculateBalance, isHexPublicKey } from '@/util/validate' import { RIGHTS } from '@/auth/RIGHTS' @@ -34,6 +32,8 @@ import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualT import Decimal from 'decimal.js-light' import { calculateDecay } from '@/util/decay' +import GdtResolver from './GdtResolver' + export const executeTransaction = async ( amount: Decimal, memo: string, @@ -143,19 +143,8 @@ export class TransactionResolver { ) // get GDT - let balanceGDT = null - try { - const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, { - email: user.email, - }) - if (!resultGDTSum.success) { - throw new Error('Call not successful') - } - balanceGDT = Number(resultGDTSum.data.sum) || 0 - } catch (err: any) { - // eslint-disable-next-line no-console - console.log('Could not query GDT Server', err) - } + gdtResolver = new GdtResolver() + const balanceGDT = await gdtResolver.gdtSum() if (!lastTransaction) { return new TransactionList(new Decimal(0), [], 0, 0, balanceGDT) From f46bd23c11426200849819345e377c5fe619f5d3 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 23 Mar 2022 19:05:19 +0100 Subject: [PATCH 13/41] refactor balance model --- backend/src/graphql/model/Balance.ts | 47 +++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/backend/src/graphql/model/Balance.ts b/backend/src/graphql/model/Balance.ts index 2f1eeb406..6a93a5b63 100644 --- a/backend/src/graphql/model/Balance.ts +++ b/backend/src/graphql/model/Balance.ts @@ -1,22 +1,55 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { ObjectType, Field } from 'type-graphql' import Decimal from 'decimal.js-light' +import CONFIG from '@/config' @ObjectType() export class Balance { - constructor(json: any) { - this.balance = json.balance - this.decay = json.decay - this.decayDate = json.decay_date + constructor(data: { + balance: Decimal + decay: Decimal + lastBookedBalance: Decimal + balanceGDT: number | null + count: number + linkCount: number + decayStartBlock?: Date + lastBookedDate?: Date | null + }) { + this.balance = data.balance + this.decay = data.decay + this.lastBookedBalance = data.lastBookedBalance + this.balanceGDT = data.balanceGDT || null + this.count = data.count + this.linkCount = data.linkCount + this.decayStartBlock = data.decayStartBlock || CONFIG.DECAY_START_TIME + this.lastBookedDate = data.lastBookedDate || null } + // the actual balance, decay included @Field(() => Decimal) balance: Decimal + // the decay since the last booked balance @Field(() => Decimal) decay: Decimal + @Field(() => Decimal) + lastBookedBalance: Decimal + + @Field(() => Number, { nullable: true }) + balanceGDT: number | null + + // the count of all transactions + @Field(() => Number) + count: number + + // the count of transaction links + @Field(() => Number) + linkCount: number + @Field(() => Date) - decayDate: Date + decayStartBlock: Date + + // may be null as there may be no transaction + @Field(() => Date, { nullable: true }) + lastBookedDate: Date | null } From a1ac6600b7b79c393448ee9f6ee04475f697c623 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 23 Mar 2022 19:06:12 +0100 Subject: [PATCH 14/41] refactor resolvers to use balance resolver and model --- .../src/graphql/resolver/BalanceResolver.ts | 39 ++++++++++++++++--- backend/src/graphql/resolver/GdtResolver.ts | 6 +-- .../graphql/resolver/TransactionResolver.ts | 6 +-- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/backend/src/graphql/resolver/BalanceResolver.ts b/backend/src/graphql/resolver/BalanceResolver.ts index 09d2fdc92..5093d4347 100644 --- a/backend/src/graphql/resolver/BalanceResolver.ts +++ b/backend/src/graphql/resolver/BalanceResolver.ts @@ -5,18 +5,23 @@ import { Resolver, Query, Ctx, Authorized } from 'type-graphql' import { Balance } from '@model/Balance' import { calculateDecay } from '@/util/decay' import { RIGHTS } from '@/auth/RIGHTS' -import { Transaction } from '@entity/Transaction' +import { Transaction, Transaction as dbTransaction } from '@entity/Transaction' import Decimal from 'decimal.js-light' +import { GdtResolver } from './GdtResolver' +import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' +import { MoreThan } from '@dbTools/typeorm' @Resolver() export class BalanceResolver { @Authorized([RIGHTS.BALANCE]) @Query(() => Balance) async balance(@Ctx() context: any): Promise { - // load user and balance const { user } = context const now = new Date() + const gdtResolver = new GdtResolver() + const balanceGDT = await gdtResolver.gdtSum(context) + const lastTransaction = await Transaction.findOne( { userId: user.id }, { order: { balanceDate: 'DESC' } }, @@ -27,14 +32,36 @@ export class BalanceResolver { return new Balance({ balance: new Decimal(0), decay: new Decimal(0), - decay_date: now.toString(), + lastBookedBalance: new Decimal(0), + balanceGDT, + count: 0, + linkCount: 0, }) } + const count = await dbTransaction.count({ where: { userId: user.id } }) + const linkCount = await dbTransactionLink.count({ + where: { + userId: user.id, + redeemedAt: null, + validUntil: MoreThan(new Date()), + }, + }) + + const calculatedDecay = calculateDecay( + lastTransaction.balance, + lastTransaction.balanceDate, + now, + ) + return new Balance({ - balance: lastTransaction.balance, - decay: calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance, - decay_date: now.toString(), + balance: calculatedDecay.balance, + decay: calculatedDecay.decay, + lastBookedBalance: lastTransaction.balance, + balanceGDT, + count, + linkCount, + lastBookedDate: lastTransaction.balanceDate, }) } } diff --git a/backend/src/graphql/resolver/GdtResolver.ts b/backend/src/graphql/resolver/GdtResolver.ts index 302b17a4a..a563a0845 100644 --- a/backend/src/graphql/resolver/GdtResolver.ts +++ b/backend/src/graphql/resolver/GdtResolver.ts @@ -5,7 +5,7 @@ import { Resolver, Query, Args, Ctx, Authorized, Arg } from 'type-graphql' import CONFIG from '@/config' import { GdtEntryList } from '@model/GdtEntryList' import Paginated from '@arg/Paginated' -import { apiGet } from '@/apis/HttpRequest' +import { apiGet, apiPost } from '@/apis/HttpRequest' import { Order } from '@enum/Order' import { RIGHTS } from '@/auth/RIGHTS' @@ -34,7 +34,7 @@ export class GdtResolver { } @Authorized([RIGHTS.GDT_SUM]) - @Query(() => Number | null) + @Query(() => Number) async gdtSum(@Ctx() context: any): Promise { const { user } = context try { @@ -48,7 +48,7 @@ export class GdtResolver { } catch (err: any) { // eslint-disable-next-line no-console console.log('Could not query GDT Server', err) - return null + return 0 } } diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 3164eaf8e..8109ba70a 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -32,7 +32,7 @@ import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualT import Decimal from 'decimal.js-light' import { calculateDecay } from '@/util/decay' -import GdtResolver from './GdtResolver' +import { GdtResolver } from './GdtResolver' export const executeTransaction = async ( amount: Decimal, @@ -143,8 +143,8 @@ export class TransactionResolver { ) // get GDT - gdtResolver = new GdtResolver() - const balanceGDT = await gdtResolver.gdtSum() + const gdtResolver = new GdtResolver() + const balanceGDT = await gdtResolver.gdtSum(context) if (!lastTransaction) { return new TransactionList(new Decimal(0), [], 0, 0, balanceGDT) From 3a8229f60f8388f6c1e724c8ce2da575d28b8c0d Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 23 Mar 2022 19:18:18 +0100 Subject: [PATCH 15/41] include hold available amount in balance calculation --- backend/src/graphql/resolver/BalanceResolver.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/BalanceResolver.ts b/backend/src/graphql/resolver/BalanceResolver.ts index 5093d4347..4867da54d 100644 --- a/backend/src/graphql/resolver/BalanceResolver.ts +++ b/backend/src/graphql/resolver/BalanceResolver.ts @@ -9,7 +9,8 @@ import { Transaction, Transaction as dbTransaction } from '@entity/Transaction' import Decimal from 'decimal.js-light' import { GdtResolver } from './GdtResolver' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' -import { MoreThan } from '@dbTools/typeorm' +import { MoreThan, getCustomRepository } from '@dbTools/typeorm' +import { TransactionLinkRepository } from '@repository/TransactionLink' @Resolver() export class BalanceResolver { @@ -48,8 +49,11 @@ export class BalanceResolver { }, }) + const transactionLinkRepository = getCustomRepository(TransactionLinkRepository) + const { sumHoldAvailableAmount } = await transactionLinkRepository.summary(user.id, now) + const calculatedDecay = calculateDecay( - lastTransaction.balance, + lastTransaction.balance.minus(sumHoldAvailableAmount.toString()), lastTransaction.balanceDate, now, ) From 0c0ce1b579dc68a35bb1a927dc9df05844820a0e Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 24 Mar 2022 14:05:23 +0100 Subject: [PATCH 16/41] add new env variable to define email optin request time delay --- backend/.env.dist | 3 ++- backend/src/config/index.ts | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/.env.dist b/backend/.env.dist index 0555bf9f5..5e3a89f82 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -1,4 +1,4 @@ -CONFIG_VERSION=v1.2022-03-18 +CONFIG_VERSION=v2.2022-03-24 # Server PORT=4000 @@ -43,6 +43,7 @@ EMAIL_SMTP_PORT=587 EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{optin}{code} EMAIL_LINK_SETPASSWORD=http://localhost/reset/{code} EMAIL_CODE_VALID_TIME=1440 +EMAIL_CODE_REQUEST_TIME=10 # Webhook WEBHOOK_ELOPAGE_SECRET=secret \ No newline at end of file diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 4e36a6910..158983d5c 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -14,7 +14,7 @@ const constants = { DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0 CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v1.2022-03-18', + EXPECTED: 'v2.2022-03-24', CURRENT: '', }, } @@ -73,6 +73,9 @@ const email = { EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME ? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 1440 : 1440, + EMAIL_CODE_REQUEST_TIME: process.env.EMAIL_CODE_REQUEST_TIME + ? parseInt(process.env.EMAIL_CODE_REQUEST_TIME) || 10 + : 10, } const webhook = { From ac000296da69d3e348766cca8e9f0ed8b38bdb66 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 24 Mar 2022 17:37:07 +0100 Subject: [PATCH 17/41] enums for optin types --- backend/src/graphql/enum/OptinType.ts | 11 +++++++++++ backend/src/graphql/resolver/UserResolver.ts | 18 +++++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 backend/src/graphql/enum/OptinType.ts diff --git a/backend/src/graphql/enum/OptinType.ts b/backend/src/graphql/enum/OptinType.ts new file mode 100644 index 000000000..7e7edde93 --- /dev/null +++ b/backend/src/graphql/enum/OptinType.ts @@ -0,0 +1,11 @@ +import { registerEnumType } from 'type-graphql' + +export enum OptinType { + EMAIL_OPT_IN_REGISTER = 1, + EMAIL_OPT_IN_RESET_PASSWORD = 2, +} + +registerEnumType(OptinType, { + name: 'OptinType', // this one is mandatory + description: 'Type of the email optin', // this one is optional +}) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index c9060384a..a1d6d6e1d 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -15,6 +15,7 @@ import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs' import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware' import { UserSettingRepository } from '@repository/UserSettingRepository' import { Setting } from '@enum/Setting' +import { OptinType } from '@enum/OptinType' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail' import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' @@ -24,9 +25,6 @@ import { ROLE_ADMIN } from '@/auth/ROLES' import { hasElopageBuys } from '@/util/hasElopageBuys' import { ServerUser } from '@entity/ServerUser' -const EMAIL_OPT_IN_RESET_PASSWORD = 2 -const EMAIL_OPT_IN_REGISTER = 1 - // eslint-disable-next-line @typescript-eslint/no-var-requires const sodium = require('sodium-native') // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -148,14 +146,16 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B return message } + const createEmailOptIn = async ( loginUserId: number, queryRunner: QueryRunner, ): Promise => { let emailOptIn = await LoginEmailOptIn.findOne({ userId: loginUserId, - emailOptInTypeId: EMAIL_OPT_IN_REGISTER, + emailOptInTypeId: OptinType.EMAIL_OPT_IN_REGISTER, }) + if (emailOptIn) { if (isOptInCodeValid(emailOptIn)) { throw new Error(`email already sent less than ${printEmailCodeValidTime()} ago`) @@ -166,7 +166,7 @@ const createEmailOptIn = async ( emailOptIn = new LoginEmailOptIn() emailOptIn.verificationCode = random(64) emailOptIn.userId = loginUserId - emailOptIn.emailOptInTypeId = EMAIL_OPT_IN_REGISTER + emailOptIn.emailOptInTypeId = OptinType.EMAIL_OPT_IN_REGISTER } await queryRunner.manager.save(emailOptIn).catch((error) => { // eslint-disable-next-line no-console @@ -179,7 +179,7 @@ const createEmailOptIn = async ( const getOptInCode = async (loginUserId: number): Promise => { let optInCode = await LoginEmailOptIn.findOne({ userId: loginUserId, - emailOptInTypeId: EMAIL_OPT_IN_RESET_PASSWORD, + emailOptInTypeId: OptinType.EMAIL_OPT_IN_RESET_PASSWORD, }) // Check for `CONFIG.EMAIL_CODE_VALID_TIME` minute delay @@ -193,7 +193,7 @@ const getOptInCode = async (loginUserId: number): Promise => { optInCode = new LoginEmailOptIn() optInCode.verificationCode = random(64) optInCode.userId = loginUserId - optInCode.emailOptInTypeId = EMAIL_OPT_IN_RESET_PASSWORD + optInCode.emailOptInTypeId = OptinType.EMAIL_OPT_IN_RESET_PASSWORD } await LoginEmailOptIn.save(optInCode) return optInCode @@ -398,7 +398,7 @@ export class UserResolver { return new User(dbUser) } - // THis is used by the admin only - should we move it to the admin resolver? + // This is used by the admin only - should we move it to the admin resolver? @Authorized([RIGHTS.SEND_ACTIVATION_EMAIL]) @Mutation(() => Boolean) async sendActivationEmail(@Arg('email') email: string): Promise { @@ -553,7 +553,7 @@ export class UserResolver { // Sign into Klicktipp // TODO do we always signUp the user? How to handle things with old users? - if (optInCode.emailOptInTypeId === EMAIL_OPT_IN_REGISTER) { + if (optInCode.emailOptInTypeId === OptinType.EMAIL_OPT_IN_REGISTER) { try { await klicktippSignIn(user.email, user.language, user.firstName, user.lastName) } catch { From 8718fdee7b0d3c97d0510cea613c05684dba4a2b Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 24 Mar 2022 17:38:03 +0100 Subject: [PATCH 18/41] add comentary to explain the meaning of the constants --- backend/src/config/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 158983d5c..caedac08e 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -70,9 +70,11 @@ const email = { process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{optin}{code}', EMAIL_LINK_SETPASSWORD: process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{optin}', + // time in minutes a optin code is valid EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME ? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 1440 : 1440, + // time in minutes that must pass to request a new optin code EMAIL_CODE_REQUEST_TIME: process.env.EMAIL_CODE_REQUEST_TIME ? parseInt(process.env.EMAIL_CODE_REQUEST_TIME) || 10 : 10, From 7cd920686db91f4a53ade117b0d23c21cc034a41 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 24 Mar 2022 17:50:34 +0100 Subject: [PATCH 19/41] helper function to create a new email optin object. Use this in create user --- backend/src/graphql/resolver/UserResolver.ts | 23 +++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index a1d6d6e1d..579940c2b 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -147,12 +147,20 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B return message } +const newEmailOptin = (userId: number): LoginEmailOptIn => { + const emailOptIn = new LoginEmailOptIn() + emailOptIn.verificationCode = random(64) + emailOptIn.userId = userId + emailOptIn.emailOptInTypeId = OptinType.EMAIL_OPT_IN_REGISTER + return emailOptIn +} + const createEmailOptIn = async ( - loginUserId: number, + userId: number, queryRunner: QueryRunner, ): Promise => { let emailOptIn = await LoginEmailOptIn.findOne({ - userId: loginUserId, + userId, emailOptInTypeId: OptinType.EMAIL_OPT_IN_REGISTER, }) @@ -165,7 +173,7 @@ const createEmailOptIn = async ( } else { emailOptIn = new LoginEmailOptIn() emailOptIn.verificationCode = random(64) - emailOptIn.userId = loginUserId + emailOptIn.userId = userId emailOptIn.emailOptInTypeId = OptinType.EMAIL_OPT_IN_REGISTER } await queryRunner.manager.save(emailOptIn).catch((error) => { @@ -363,9 +371,12 @@ export class UserResolver { throw new Error('error saving user') }) - // Store EmailOptIn in DB - // TODO: this has duplicate code with sendResetPasswordEmail - const emailOptIn = await createEmailOptIn(dbUser.id, queryRunner) + const emailOptIn = newEmailOptin(dbUser.id) + await queryRunner.manager.save(emailOptIn).catch((error) => { + // eslint-disable-next-line no-console + console.log('Error while saving emailOptIn', error) + throw new Error('error saving email opt in') + }) const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace( /{optin}/g, From 965ab22bab4ded3acd1a3aa055af8df3389b4f4d Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 24 Mar 2022 18:17:46 +0100 Subject: [PATCH 20/41] create helper functions for time management, rename some functions --- .../src/graphql/resolver/UserResolver.test.ts | 13 ++--- backend/src/graphql/resolver/UserResolver.ts | 53 ++++++++++++------- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index d98621246..726ecfb09 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -11,7 +11,7 @@ import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { User } from '@entity/User' import CONFIG from '@/config' import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' -import { printEmailCodeValidTime } from './UserResolver' +import { printTimeDuration } from './UserResolver' // import { klicktippSignIn } from '@/apis/KlicktippController' @@ -417,19 +417,16 @@ describe('UserResolver', () => { }) }) -describe('printEmailCodeValidTime', () => { +describe('printTimeDuration', () => { it('works with 10 minutes', () => { - CONFIG.EMAIL_CODE_VALID_TIME = 10 - expect(printEmailCodeValidTime()).toBe('10 minutes') + expect(printTimeDuration(10)).toBe('10 minutes') }) it('works with 1440 minutes', () => { - CONFIG.EMAIL_CODE_VALID_TIME = 1440 - expect(printEmailCodeValidTime()).toBe('24 hours') + expect(printTimeDuration(1440)).toBe('24 hours') }) it('works with 1410 minutes', () => { - CONFIG.EMAIL_CODE_VALID_TIME = 1410 - expect(printEmailCodeValidTime()).toBe('23 hours and 30 minutes') + expect(printTimeDuration(1410)).toBe('23 hours and 30 minutes') }) }) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 579940c2b..c9f5b6564 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -165,8 +165,10 @@ const createEmailOptIn = async ( }) if (emailOptIn) { - if (isOptInCodeValid(emailOptIn)) { - throw new Error(`email already sent less than ${printEmailCodeValidTime()} ago`) + if (isOptinValid(emailOptIn)) { + throw new Error( + `email already sent less than ${printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)} ago`, + ) } emailOptIn.updatedAt = new Date() emailOptIn.resendCount++ @@ -192,8 +194,10 @@ const getOptInCode = async (loginUserId: number): Promise => { // Check for `CONFIG.EMAIL_CODE_VALID_TIME` minute delay if (optInCode) { - if (isOptInCodeValid(optInCode)) { - throw new Error(`email already sent less than $(printEmailCodeValidTime()} minutes ago`) + if (isOptinValid(optInCode)) { + throw new Error( + `email already sent less than $(printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)} minutes ago`, + ) } optInCode.updatedAt = new Date() optInCode.resendCount++ @@ -505,8 +509,10 @@ export class UserResolver { }) // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes - if (!isOptInCodeValid(optInCode)) { - throw new Error(`email was sent more than ${printEmailCodeValidTime()} ago`) + if (!isOptinValid(optInCode)) { + throw new Error( + `email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`, + ) } // load user @@ -584,8 +590,10 @@ export class UserResolver { async queryOptIn(@Arg('optIn') optIn: string): Promise { const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: optIn }) // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes - if (!isOptInCodeValid(optInCode)) { - throw new Error(`email was sent more than $(printEmailCodeValidTime()} ago`) + if (!isOptinValid(optInCode)) { + throw new Error( + `email was sent more than $(printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`, + ) } return true } @@ -692,23 +700,32 @@ export class UserResolver { } } -function isOptInCodeValid(optInCode: LoginEmailOptIn) { - const timeElapsed = Date.now() - new Date(optInCode.updatedAt).getTime() - return timeElapsed <= CONFIG.EMAIL_CODE_VALID_TIME * 60 * 1000 +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 emailCodeValidTime = (): { hours?: number; minutes: number } => { - if (CONFIG.EMAIL_CODE_VALID_TIME > 60) { +const isOptinValid = (optin: LoginEmailOptIn): boolean => { + return isTimeExpired(optin, CONFIG.EMAIL_CODE_VALID_TIME) +} + +const canResendOptin = (optin: LoginEmailOptIn): boolean => { + return isTimeExpired(optin, CONFIG.EMAIL_CODE_REQUEST_TIME) +} + +const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => { + if (time > 60) { return { - hours: Math.floor(CONFIG.EMAIL_CODE_VALID_TIME / 60), - minutes: CONFIG.EMAIL_CODE_VALID_TIME % 60, + hours: Math.floor(time / 60), + minutes: time % 60, } } - return { minutes: CONFIG.EMAIL_CODE_VALID_TIME } + return { minutes: time } } -export const printEmailCodeValidTime = (): string => { - const time = emailCodeValidTime() +export const printTimeDuration = (duration: number): string => { + const time = getTimeDurationObject(duration) const result = time.minutes > 0 ? `${time.minutes} minutes` : '' if (time.hours) return `${time.hours} hours` + (result !== '' ? ` and ${result}` : '') return result From 607805075c086ba3fe8153e0641337a22083a2af Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 24 Mar 2022 18:33:07 +0100 Subject: [PATCH 21/41] refactor sendActivationEmail, get rid of getOptInCode function --- backend/src/graphql/resolver/UserResolver.ts | 46 ++++++++------------ 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index c9f5b6564..50bba974f 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -186,31 +186,6 @@ const createEmailOptIn = async ( return emailOptIn } -const getOptInCode = async (loginUserId: number): Promise => { - let optInCode = await LoginEmailOptIn.findOne({ - userId: loginUserId, - emailOptInTypeId: OptinType.EMAIL_OPT_IN_RESET_PASSWORD, - }) - - // Check for `CONFIG.EMAIL_CODE_VALID_TIME` minute delay - if (optInCode) { - if (isOptinValid(optInCode)) { - throw new Error( - `email already sent less than $(printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)} minutes ago`, - ) - } - optInCode.updatedAt = new Date() - optInCode.resendCount++ - } else { - optInCode = new LoginEmailOptIn() - optInCode.verificationCode = random(64) - optInCode.userId = loginUserId - optInCode.emailOptInTypeId = OptinType.EMAIL_OPT_IN_RESET_PASSWORD - } - await LoginEmailOptIn.save(optInCode) - return optInCode -} - @Resolver() export class UserResolver { @Authorized([RIGHTS.VERIFY_LOGIN]) @@ -460,11 +435,28 @@ export class UserResolver { @Authorized([RIGHTS.SEND_RESET_PASSWORD_EMAIL]) @Query(() => Boolean) async sendResetPasswordEmail(@Arg('email') email: string): Promise { - // TODO: this has duplicate code with createUser email = email.trim().toLowerCase() const user = await DbUser.findOneOrFail({ email }) - const optInCode = await getOptInCode(user.id) + // can be both types: REGISTER and RESET_PASSWORD + let optInCode = await LoginEmailOptIn.findOne({ + userId: user.id, + }) + + if (optInCode) { + if (!canResendOptin(optInCode)) { + throw new Error( + `email already sent less than $(printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)} minutes ago`, + ) + } + optInCode.updatedAt = new Date() + optInCode.resendCount++ + } else { + optInCode = newEmailOptin(user.id) + } + // now it is RESET_PASSWORD + optInCode.emailOptInTypeId = OptinType.EMAIL_OPT_IN_RESET_PASSWORD + await LoginEmailOptIn.save(optInCode) const link = CONFIG.EMAIL_LINK_SETPASSWORD.replace( /{optin}/g, From 1e3300ad5a2c276786089c25a5bc9993496e268b Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 24 Mar 2022 18:38:24 +0100 Subject: [PATCH 22/41] use optIn as naming convention --- .../enum/{OptinType.ts => OptInType.ts} | 6 +-- backend/src/graphql/resolver/UserResolver.ts | 38 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) rename backend/src/graphql/enum/{OptinType.ts => OptInType.ts} (64%) diff --git a/backend/src/graphql/enum/OptinType.ts b/backend/src/graphql/enum/OptInType.ts similarity index 64% rename from backend/src/graphql/enum/OptinType.ts rename to backend/src/graphql/enum/OptInType.ts index 7e7edde93..2dd2d07b0 100644 --- a/backend/src/graphql/enum/OptinType.ts +++ b/backend/src/graphql/enum/OptInType.ts @@ -1,11 +1,11 @@ import { registerEnumType } from 'type-graphql' -export enum OptinType { +export enum OptInType { EMAIL_OPT_IN_REGISTER = 1, EMAIL_OPT_IN_RESET_PASSWORD = 2, } -registerEnumType(OptinType, { - name: 'OptinType', // this one is mandatory +registerEnumType(OptInType, { + name: 'OptInType', // this one is mandatory description: 'Type of the email optin', // this one is optional }) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 50bba974f..9f48049e1 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -15,7 +15,7 @@ import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs' import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware' import { UserSettingRepository } from '@repository/UserSettingRepository' import { Setting } from '@enum/Setting' -import { OptinType } from '@enum/OptinType' +import { OptInType } from '@enum/OptInType' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail' import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' @@ -147,11 +147,11 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B return message } -const newEmailOptin = (userId: number): LoginEmailOptIn => { +const newEmailOptIn = (userId: number): LoginEmailOptIn => { const emailOptIn = new LoginEmailOptIn() emailOptIn.verificationCode = random(64) emailOptIn.userId = userId - emailOptIn.emailOptInTypeId = OptinType.EMAIL_OPT_IN_REGISTER + emailOptIn.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER return emailOptIn } @@ -161,11 +161,11 @@ const createEmailOptIn = async ( ): Promise => { let emailOptIn = await LoginEmailOptIn.findOne({ userId, - emailOptInTypeId: OptinType.EMAIL_OPT_IN_REGISTER, + emailOptInTypeId: OptInType.EMAIL_OPT_IN_REGISTER, }) if (emailOptIn) { - if (isOptinValid(emailOptIn)) { + if (isOptInValid(emailOptIn)) { throw new Error( `email already sent less than ${printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)} ago`, ) @@ -176,7 +176,7 @@ const createEmailOptIn = async ( emailOptIn = new LoginEmailOptIn() emailOptIn.verificationCode = random(64) emailOptIn.userId = userId - emailOptIn.emailOptInTypeId = OptinType.EMAIL_OPT_IN_REGISTER + emailOptIn.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER } await queryRunner.manager.save(emailOptIn).catch((error) => { // eslint-disable-next-line no-console @@ -350,7 +350,7 @@ export class UserResolver { throw new Error('error saving user') }) - const emailOptIn = newEmailOptin(dbUser.id) + const emailOptIn = newEmailOptIn(dbUser.id) await queryRunner.manager.save(emailOptIn).catch((error) => { // eslint-disable-next-line no-console console.log('Error while saving emailOptIn', error) @@ -444,7 +444,7 @@ export class UserResolver { }) if (optInCode) { - if (!canResendOptin(optInCode)) { + if (!canResendOptIn(optInCode)) { throw new Error( `email already sent less than $(printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)} minutes ago`, ) @@ -452,10 +452,10 @@ export class UserResolver { optInCode.updatedAt = new Date() optInCode.resendCount++ } else { - optInCode = newEmailOptin(user.id) + optInCode = newEmailOptIn(user.id) } // now it is RESET_PASSWORD - optInCode.emailOptInTypeId = OptinType.EMAIL_OPT_IN_RESET_PASSWORD + optInCode.emailOptInTypeId = OptInType.EMAIL_OPT_IN_RESET_PASSWORD await LoginEmailOptIn.save(optInCode) const link = CONFIG.EMAIL_LINK_SETPASSWORD.replace( @@ -501,7 +501,7 @@ export class UserResolver { }) // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes - if (!isOptinValid(optInCode)) { + if (!isOptInValid(optInCode)) { throw new Error( `email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`, ) @@ -562,7 +562,7 @@ export class UserResolver { // Sign into Klicktipp // TODO do we always signUp the user? How to handle things with old users? - if (optInCode.emailOptInTypeId === OptinType.EMAIL_OPT_IN_REGISTER) { + if (optInCode.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) { try { await klicktippSignIn(user.email, user.language, user.firstName, user.lastName) } catch { @@ -582,7 +582,7 @@ export class UserResolver { async queryOptIn(@Arg('optIn') optIn: string): Promise { const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: optIn }) // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes - if (!isOptinValid(optInCode)) { + if (!isOptInValid(optInCode)) { throw new Error( `email was sent more than $(printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`, ) @@ -692,18 +692,18 @@ export class UserResolver { } } -const isTimeExpired = (optin: LoginEmailOptIn, duration: number): boolean => { - const timeElapsed = Date.now() - new Date(optin.updatedAt).getTime() +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 isOptinValid = (optin: LoginEmailOptIn): boolean => { - return isTimeExpired(optin, CONFIG.EMAIL_CODE_VALID_TIME) +const isOptInValid = (optIn: LoginEmailOptIn): boolean => { + return isTimeExpired(optIn, CONFIG.EMAIL_CODE_VALID_TIME) } -const canResendOptin = (optin: LoginEmailOptIn): boolean => { - return isTimeExpired(optin, CONFIG.EMAIL_CODE_REQUEST_TIME) +const canResendOptIn = (optIn: LoginEmailOptIn): boolean => { + return isTimeExpired(optIn, CONFIG.EMAIL_CODE_REQUEST_TIME) } const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => { From 9da2047e8c0899b3210bc7e255d5e3dbb5da00cc Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 24 Mar 2022 19:14:59 +0100 Subject: [PATCH 23/41] move sendActivationEmail mutation from user to admin resolver --- backend/src/graphql/resolver/AdminResolver.ts | 36 ++++++ backend/src/graphql/resolver/UserResolver.ts | 106 ++++-------------- 2 files changed, 57 insertions(+), 85 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index d98b38b7f..061c485c5 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -34,6 +34,8 @@ import { Decay } from '@model/Decay' import Paginated from '@arg/Paginated' import { Order } from '@enum/Order' import { communityUser } from '@/util/communityUser' +import { checkExistingOptInCode, activationLink } from './UserResolver' +import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' // const EMAIL_OPT_IN_REGISTER = 1 // const EMAIL_OPT_UNKNOWN = 3 // elopage? @@ -369,6 +371,40 @@ export class AdminResolver { const user = await dbUser.findOneOrFail({ id: userId }) return userTransactions.map((t) => new Transaction(t, new User(user), communityUser)) } + + @Authorized([RIGHTS.SEND_ACTIVATION_EMAIL]) + @Mutation(() => Boolean) + async sendActivationEmail(@Arg('email') email: string): Promise { + email = email.trim().toLowerCase() + const user = await dbUser.findOneOrFail({ email: email }) + + // can be both types: REGISTER and RESET_PASSWORD + let optInCode = await LoginEmailOptIn.findOne({ + userId: user.id, + }) + + optInCode = checkExistingOptInCode(optInCode, user.id) + // keep the optin type (when newly created is is REGISTER) + await LoginEmailOptIn.save(optInCode) + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const emailSent = await sendAccountActivationEmail({ + link: activationLink(optInCode), + firstName: user.firstName, + lastName: user.lastName, + email, + }) + + /* 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) { + // eslint-disable-next-line no-console + console.log(`Account confirmation link: ${activationLink}`) + } + */ + + return true + } } interface CreationMap { diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 9f48049e1..7cce31eab 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -3,7 +3,7 @@ import fs from 'fs' import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql' -import { getConnection, getCustomRepository, QueryRunner } from '@dbTools/typeorm' +import { getConnection, getCustomRepository } from '@dbTools/typeorm' import CONFIG from '@/config' import { User } from '@model/User' import { User as DbUser } from '@entity/User' @@ -155,35 +155,29 @@ const newEmailOptIn = (userId: number): LoginEmailOptIn => { return emailOptIn } -const createEmailOptIn = async ( +// needed by AdminResolver +// checks if given code exists and can be resent +// if optIn does not exits, it is created +export const checkExistingOptInCode = ( + optInCode: LoginEmailOptIn | undefined, userId: number, - queryRunner: QueryRunner, -): Promise => { - let emailOptIn = await LoginEmailOptIn.findOne({ - userId, - emailOptInTypeId: OptInType.EMAIL_OPT_IN_REGISTER, - }) - - if (emailOptIn) { - if (isOptInValid(emailOptIn)) { +): LoginEmailOptIn => { + if (optInCode) { + if (!canResendOptIn(optInCode)) { throw new Error( - `email already sent less than ${printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)} ago`, + `email already sent less than $(printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)} minutes ago`, ) } - emailOptIn.updatedAt = new Date() - emailOptIn.resendCount++ + optInCode.updatedAt = new Date() + optInCode.resendCount++ } else { - emailOptIn = new LoginEmailOptIn() - emailOptIn.verificationCode = random(64) - emailOptIn.userId = userId - emailOptIn.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER + optInCode = newEmailOptIn(userId) } - await queryRunner.manager.save(emailOptIn).catch((error) => { - // eslint-disable-next-line no-console - console.log('Error while saving emailOptIn', error) - throw new Error('error saving email opt in') - }) - return emailOptIn + return optInCode +} + +export const activationLink = (optInCode: LoginEmailOptIn): string => { + return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, optInCode.verificationCode.toString()) } @Resolver() @@ -388,50 +382,6 @@ export class UserResolver { return new User(dbUser) } - // This is used by the admin only - should we move it to the admin resolver? - @Authorized([RIGHTS.SEND_ACTIVATION_EMAIL]) - @Mutation(() => Boolean) - async sendActivationEmail(@Arg('email') email: string): Promise { - email = email.trim().toLowerCase() - const user = await DbUser.findOneOrFail({ email: email }) - - const queryRunner = getConnection().createQueryRunner() - await queryRunner.connect() - await queryRunner.startTransaction('READ UNCOMMITTED') - - try { - const emailOptIn = await createEmailOptIn(user.id, queryRunner) - - const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace( - /{optin}/g, - emailOptIn.verificationCode.toString(), - ) - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const emailSent = await sendAccountActivationEmail({ - link: activationLink, - firstName: user.firstName, - lastName: user.lastName, - email, - }) - - /* 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) { - // eslint-disable-next-line no-console - console.log(`Account confirmation link: ${activationLink}`) - } - */ - await queryRunner.commitTransaction() - } catch (e) { - await queryRunner.rollbackTransaction() - throw e - } finally { - await queryRunner.release() - } - return true - } - @Authorized([RIGHTS.SEND_RESET_PASSWORD_EMAIL]) @Query(() => Boolean) async sendResetPasswordEmail(@Arg('email') email: string): Promise { @@ -443,29 +393,14 @@ export class UserResolver { userId: user.id, }) - if (optInCode) { - if (!canResendOptIn(optInCode)) { - throw new Error( - `email already sent less than $(printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)} minutes ago`, - ) - } - optInCode.updatedAt = new Date() - optInCode.resendCount++ - } else { - optInCode = newEmailOptIn(user.id) - } + optInCode = checkExistingOptInCode(optInCode, user.id) // now it is RESET_PASSWORD optInCode.emailOptInTypeId = OptInType.EMAIL_OPT_IN_RESET_PASSWORD await LoginEmailOptIn.save(optInCode) - const link = CONFIG.EMAIL_LINK_SETPASSWORD.replace( - /{optin}/g, - optInCode.verificationCode.toString(), - ) - // eslint-disable-next-line @typescript-eslint/no-unused-vars const emailSent = await sendResetPasswordEmail({ - link, + link: activationLink(optInCode), firstName: user.firstName, lastName: user.lastName, email, @@ -547,6 +482,7 @@ export class UserResolver { throw new Error('error saving user: ' + error) }) + // why do we delete the code? // Delete Code await queryRunner.manager.remove(optInCode).catch((error) => { throw new Error('error deleting code: ' + error) From 5456affb9c8db21a8b5a96dcf0355f3c2450770a Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 24 Mar 2022 19:27:32 +0100 Subject: [PATCH 24/41] fix logic (not canResendOptIn), fix error strings --- backend/src/graphql/resolver/UserResolver.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 7cce31eab..7d465c213 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -165,7 +165,9 @@ export const checkExistingOptInCode = ( if (optInCode) { if (!canResendOptIn(optInCode)) { throw new Error( - `email already sent less than $(printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)} minutes ago`, + `email already sent less than ${printTimeDuration( + CONFIG.EMAIL_CODE_REQUEST_TIME, + )} minutes ago`, ) } optInCode.updatedAt = new Date() @@ -520,7 +522,7 @@ export class UserResolver { // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes if (!isOptInValid(optInCode)) { 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`, ) } return true @@ -639,7 +641,7 @@ const isOptInValid = (optIn: LoginEmailOptIn): boolean => { } const canResendOptIn = (optIn: LoginEmailOptIn): boolean => { - return isTimeExpired(optIn, CONFIG.EMAIL_CODE_REQUEST_TIME) + return !isTimeExpired(optIn, CONFIG.EMAIL_CODE_REQUEST_TIME) } const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => { From cb902493018ff083bbff07d2fe4fbad687cc9b73 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 29 Mar 2022 00:50:50 +0200 Subject: [PATCH 25/41] rename function, add parameter for optinType, sort by updatedAt DESC --- backend/src/graphql/resolver/AdminResolver.ts | 9 ++++----- backend/src/graphql/resolver/UserResolver.ts | 14 ++++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 061c485c5..7f966b275 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -34,7 +34,7 @@ import { Decay } from '@model/Decay' import Paginated from '@arg/Paginated' import { Order } from '@enum/Order' import { communityUser } from '@/util/communityUser' -import { checkExistingOptInCode, activationLink } from './UserResolver' +import { checkOptInCode, activationLink } from './UserResolver' import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' // const EMAIL_OPT_IN_REGISTER = 1 @@ -380,12 +380,11 @@ export class AdminResolver { // can be both types: REGISTER and RESET_PASSWORD let optInCode = await LoginEmailOptIn.findOne({ - userId: user.id, + where: { userId: user.id }, + order: { updatedAt: 'DESC' }, }) - optInCode = checkExistingOptInCode(optInCode, user.id) - // keep the optin type (when newly created is is REGISTER) - await LoginEmailOptIn.save(optInCode) + optInCode = await checkOptInCode(optInCode, user.id) // eslint-disable-next-line @typescript-eslint/no-unused-vars const emailSent = await sendAccountActivationEmail({ diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 7d465c213..81ca42d5c 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -158,10 +158,11 @@ const newEmailOptIn = (userId: number): LoginEmailOptIn => { // needed by AdminResolver // checks if given code exists and can be resent // if optIn does not exits, it is created -export const checkExistingOptInCode = ( +export const checkOptInCode = async ( optInCode: LoginEmailOptIn | undefined, userId: number, -): LoginEmailOptIn => { + optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER, +): Promise => { if (optInCode) { if (!canResendOptIn(optInCode)) { throw new Error( @@ -175,6 +176,10 @@ export const checkExistingOptInCode = ( } else { optInCode = newEmailOptIn(userId) } + optInCode.emailOptInTypeId = optInType + await LoginEmailOptIn.save(optInCode).catch(() => { + throw new Error('Unable to save optin code.') + }) return optInCode } @@ -395,10 +400,7 @@ export class UserResolver { userId: user.id, }) - optInCode = checkExistingOptInCode(optInCode, user.id) - // now it is RESET_PASSWORD - optInCode.emailOptInTypeId = OptInType.EMAIL_OPT_IN_RESET_PASSWORD - await LoginEmailOptIn.save(optInCode) + optInCode = await checkOptInCode(optInCode, user.id, OptInType.EMAIL_OPT_IN_RESET_PASSWORD) // eslint-disable-next-line @typescript-eslint/no-unused-vars const emailSent = await sendResetPasswordEmail({ From 37e36e01af02a5b5dd8f1b52c7015efbdcec5c69 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 29 Mar 2022 00:51:44 +0200 Subject: [PATCH 26/41] do not remove optin code in DB after email verification --- backend/src/graphql/resolver/UserResolver.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 81ca42d5c..07cd90a23 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -486,12 +486,6 @@ export class UserResolver { throw new Error('error saving user: ' + error) }) - // why do we delete the code? - // Delete Code - await queryRunner.manager.remove(optInCode).catch((error) => { - throw new Error('error deleting code: ' + error) - }) - await queryRunner.commitTransaction() } catch (e) { await queryRunner.rollbackTransaction() From d584651fc10fe61ea65de98b614a8ce19480707b Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 29 Mar 2022 01:11:25 +0200 Subject: [PATCH 27/41] rename right to GDT_BALANCE --- backend/src/auth/RIGHTS.ts | 2 +- backend/src/auth/ROLES.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index 57401d361..5cd0c421d 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -24,7 +24,7 @@ export enum RIGHTS { QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK', REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK', LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS', - GDT_SUM = 'GDT_SUM', + GDT_BALANCE = 'GDT_SUM', // Admin SEARCH_USERS = 'SEARCH_USERS', CREATE_PENDING_CREATION = 'CREATE_PENDING_CREATION', diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts index 5cb8f833a..891fe1844 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -22,7 +22,7 @@ export const ROLE_USER = new Role('user', [ RIGHTS.DELETE_TRANSACTION_LINK, RIGHTS.REDEEM_TRANSACTION_LINK, RIGHTS.LIST_TRANSACTION_LINKS, - RIGHTS.GDT_SUM, + RIGHTS.GDT_BALANCE, ]) export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights From e9e97549a7b33cd6da4e628479c5134f5ffcb859 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 29 Mar 2022 01:12:07 +0200 Subject: [PATCH 28/41] rename right to GDT_BALANCE, rename query to gdtBalance, ensure gdtBalance is null when API call fails --- backend/src/graphql/resolver/BalanceResolver.ts | 2 +- backend/src/graphql/resolver/GdtResolver.ts | 6 +++--- backend/src/graphql/resolver/TransactionResolver.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/src/graphql/resolver/BalanceResolver.ts b/backend/src/graphql/resolver/BalanceResolver.ts index 4867da54d..dfaafc8e0 100644 --- a/backend/src/graphql/resolver/BalanceResolver.ts +++ b/backend/src/graphql/resolver/BalanceResolver.ts @@ -21,7 +21,7 @@ export class BalanceResolver { const now = new Date() const gdtResolver = new GdtResolver() - const balanceGDT = await gdtResolver.gdtSum(context) + const balanceGDT = await gdtResolver.gdtBalance(context) const lastTransaction = await Transaction.findOne( { userId: user.id }, diff --git a/backend/src/graphql/resolver/GdtResolver.ts b/backend/src/graphql/resolver/GdtResolver.ts index a563a0845..e2409160b 100644 --- a/backend/src/graphql/resolver/GdtResolver.ts +++ b/backend/src/graphql/resolver/GdtResolver.ts @@ -33,9 +33,9 @@ export class GdtResolver { } } - @Authorized([RIGHTS.GDT_SUM]) + @Authorized([RIGHTS.GDT_BALANCE]) @Query(() => Number) - async gdtSum(@Ctx() context: any): Promise { + async gdtBalance(@Ctx() context: any): Promise { const { user } = context try { const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, { @@ -48,7 +48,7 @@ export class GdtResolver { } catch (err: any) { // eslint-disable-next-line no-console console.log('Could not query GDT Server', err) - return 0 + return null } } diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 1a5bf80d6..00b47658b 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -156,7 +156,7 @@ export class TransactionResolver { // get GDT const gdtResolver = new GdtResolver() - const balanceGDT = await gdtResolver.gdtSum(context) + const balanceGDT = await gdtResolver.gdtBalance(context) if (!lastTransaction) { return new TransactionList(new Decimal(0), [], 0, 0, balanceGDT) From 03da5aa34319d8ae1198f5145aba3b6b9627a012 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 29 Mar 2022 01:29:08 +0200 Subject: [PATCH 29/41] change transaction list query to new balance object --- frontend/src/graphql/queries.js | 15 ++++++++++----- frontend/src/layouts/DashboardLayout_gdd.vue | 12 +++++++----- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index e47da0fea..ecd8208b5 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -45,11 +45,16 @@ export const logout = gql` export const transactionsQuery = gql` query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) { transactionList(currentPage: $currentPage, pageSize: $pageSize, order: $order) { - balanceGDT - count - linkCount - balance - decayStartBlock + balance { + balance + decay + lastBookedBalance + balanceGDT + count + linkCount + decayStartBlock + lastBookedDate + } transactions { id typeId diff --git a/frontend/src/layouts/DashboardLayout_gdd.vue b/frontend/src/layouts/DashboardLayout_gdd.vue index 9abcf42cf..de2c68bf0 100755 --- a/frontend/src/layouts/DashboardLayout_gdd.vue +++ b/frontend/src/layouts/DashboardLayout_gdd.vue @@ -103,12 +103,14 @@ export default { data: { transactionList }, } = result this.GdtBalance = - transactionList.balanceGDT === null ? null : Number(transactionList.balanceGDT) + transactionList.balance.balanceGDT === null + ? null + : Number(transactionList.balance.balanceGDT) this.transactions = transactionList.transactions - this.balance = Number(transactionList.balance) - this.transactionCount = transactionList.count - this.transactionLinkCount = transactionList.linkCount - this.decayStartBlock = new Date(transactionList.decayStartBlock) + this.balance = Number(transactionList.balance.balance) + this.transactionCount = transactionList.balance.count + this.transactionLinkCount = transactionList.balance.linkCount + this.decayStartBlock = new Date(transactionList.balance.decayStartBlock) this.pending = false }) .catch((error) => { From dc3031531136498d699ac9082ab85798c6a844b6 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 29 Mar 2022 01:31:23 +0200 Subject: [PATCH 30/41] change resolved object to new schema --- frontend/src/layouts/DashboardLayout_gdd.spec.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/layouts/DashboardLayout_gdd.spec.js b/frontend/src/layouts/DashboardLayout_gdd.spec.js index ed2199a91..ac079b0f3 100644 --- a/frontend/src/layouts/DashboardLayout_gdd.spec.js +++ b/frontend/src/layouts/DashboardLayout_gdd.spec.js @@ -145,11 +145,13 @@ describe('DashboardLayoutGdd', () => { apolloMock.mockResolvedValue({ data: { transactionList: { - balanceGDT: 100, - count: 4, - linkCount: 8, - balance: 1450, - decay: 1250, + balance: { + balanceGDT: 100, + count: 4, + linkCount: 8, + balance: 1450, + decay: 1250, + }, transactions: ['transaction', 'transaction', 'transaction', 'transaction'], }, }, From 27ad1d2e229ac11d6ddeb577bc356adf07367616 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 29 Mar 2022 01:54:13 +0200 Subject: [PATCH 31/41] transaction list model contains balance, user balance resolver to create transacton list result --- backend/src/graphql/model/TransactionList.ts | 32 +++--------------- .../src/graphql/resolver/BalanceResolver.ts | 33 +++++++++++-------- .../graphql/resolver/TransactionResolver.ts | 23 +++++-------- 3 files changed, 32 insertions(+), 56 deletions(-) diff --git a/backend/src/graphql/model/TransactionList.ts b/backend/src/graphql/model/TransactionList.ts index 9e8356747..888c30dc7 100644 --- a/backend/src/graphql/model/TransactionList.ts +++ b/backend/src/graphql/model/TransactionList.ts @@ -1,40 +1,16 @@ import { ObjectType, Field } from 'type-graphql' -import CONFIG from '@/config' -import Decimal from 'decimal.js-light' import { Transaction } from './Transaction' +import { Balance } from './Balance' @ObjectType() export class TransactionList { - constructor( - balance: Decimal, - transactions: Transaction[], - count: number, - linkCount: number, - balanceGDT?: number | null, - decayStartBlock: Date = CONFIG.DECAY_START_TIME, - ) { + constructor(balance: Balance, transactions: Transaction[]) { this.balance = balance this.transactions = transactions - this.count = count - this.linkCount = linkCount - this.balanceGDT = balanceGDT || null - this.decayStartBlock = decayStartBlock } - @Field(() => Number, { nullable: true }) - balanceGDT: number | null - - @Field(() => Number) - count: number - - @Field(() => Number) - linkCount: number - - @Field(() => Decimal) - balance: Decimal - - @Field(() => Date) - decayStartBlock: Date + @Field(() => Balance) + balance: Balance @Field(() => [Transaction]) transactions: Transaction[] diff --git a/backend/src/graphql/resolver/BalanceResolver.ts b/backend/src/graphql/resolver/BalanceResolver.ts index dfaafc8e0..f206cb0b9 100644 --- a/backend/src/graphql/resolver/BalanceResolver.ts +++ b/backend/src/graphql/resolver/BalanceResolver.ts @@ -23,10 +23,9 @@ export class BalanceResolver { const gdtResolver = new GdtResolver() const balanceGDT = await gdtResolver.gdtBalance(context) - const lastTransaction = await Transaction.findOne( - { userId: user.id }, - { order: { balanceDate: 'DESC' } }, - ) + const lastTransaction = context.lastTransaction + ? context.lastTransaction + : await Transaction.findOne({ userId: user.id }, { order: { balanceDate: 'DESC' } }) // No balance found if (!lastTransaction) { @@ -40,17 +39,25 @@ export class BalanceResolver { }) } - const count = await dbTransaction.count({ where: { userId: user.id } }) - const linkCount = await dbTransactionLink.count({ - where: { - userId: user.id, - redeemedAt: null, - validUntil: MoreThan(new Date()), - }, - }) + const count = + context.count || context.count === 0 + ? context.count + : await dbTransaction.count({ where: { userId: user.id } }) + const linkCount = + context.linkCount || context.linkCount === 0 + ? context.linkCount + : await dbTransactionLink.count({ + where: { + userId: user.id, + redeemedAt: null, + validUntil: MoreThan(new Date()), + }, + }) const transactionLinkRepository = getCustomRepository(TransactionLinkRepository) - const { sumHoldAvailableAmount } = await transactionLinkRepository.summary(user.id, now) + const { sumHoldAvailableAmount } = context.sumHoldAvailableAmount + ? { sumHoldAvailableAmount: context.sumHoldAvailableAmount } + : await transactionLinkRepository.summary(user.id, now) const calculatedDecay = calculateDecay( lastTransaction.balance.minus(sumHoldAvailableAmount.toString()), diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 00b47658b..bb7a5c2f6 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -30,9 +30,8 @@ import { User } from '@model/User' import { communityUser } from '@/util/communityUser' import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions' import Decimal from 'decimal.js-light' -import { calculateDecay } from '@/util/decay' -import { GdtResolver } from './GdtResolver' +import { BalanceResolver } from './BalanceResolver' const MEMO_MAX_CHARS = 255 const MEMO_MIN_CHARS = 5 @@ -154,12 +153,11 @@ export class TransactionResolver { { order: { balanceDate: 'DESC' } }, ) - // get GDT - const gdtResolver = new GdtResolver() - const balanceGDT = await gdtResolver.gdtBalance(context) + const balanceResolver = new BalanceResolver() + context.lastTransaction = lastTransaction if (!lastTransaction) { - return new TransactionList(new Decimal(0), [], 0, 0, balanceGDT) + return new TransactionList(await balanceResolver.balance(context), []) } // find transactions @@ -172,6 +170,7 @@ export class TransactionResolver { offset, order, ) + context.count = userTransactionsCount // find involved users; I am involved const involvedUserIds: number[] = [user.id] @@ -194,6 +193,8 @@ export class TransactionResolver { const transactionLinkRepository = getCustomRepository(TransactionLinkRepository) const { sumHoldAvailableAmount, sumAmount, lastDate, firstDate, transactionLinkcount } = await transactionLinkRepository.summary(user.id, now) + context.linkCount = transactionLinkcount + context.sumHoldAvailableAmount = sumHoldAvailableAmount // decay & link transactions if (currentPage === 1 && order === Order.DESC) { @@ -226,15 +227,7 @@ export class TransactionResolver { }) // Construct Result - return new TransactionList( - calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance.minus( - sumHoldAvailableAmount.toString(), - ), - transactions, - userTransactionsCount, - transactionLinkcount, - balanceGDT, - ) + return new TransactionList(await balanceResolver.balance(context), transactions) } @Authorized([RIGHTS.SEND_COINS]) From 51258b838eac07844959820c3ff1bedaad1cfd73 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 29 Mar 2022 01:55:59 +0200 Subject: [PATCH 32/41] set rights correctly --- backend/src/auth/RIGHTS.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index 5cd0c421d..93c7d8ea8 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -24,7 +24,7 @@ export enum RIGHTS { QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK', REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK', LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS', - GDT_BALANCE = 'GDT_SUM', + GDT_BALANCE = 'GDT_BALANCE', // Admin SEARCH_USERS = 'SEARCH_USERS', CREATE_PENDING_CREATION = 'CREATE_PENDING_CREATION', From feb89cf4e02c2033b98d27ff535d01fe12c8476b Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 29 Mar 2022 01:58:20 +0200 Subject: [PATCH 33/41] import transaction as db transaction --- backend/src/graphql/resolver/BalanceResolver.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/BalanceResolver.ts b/backend/src/graphql/resolver/BalanceResolver.ts index f206cb0b9..7d30f6868 100644 --- a/backend/src/graphql/resolver/BalanceResolver.ts +++ b/backend/src/graphql/resolver/BalanceResolver.ts @@ -5,7 +5,7 @@ import { Resolver, Query, Ctx, Authorized } from 'type-graphql' import { Balance } from '@model/Balance' import { calculateDecay } from '@/util/decay' import { RIGHTS } from '@/auth/RIGHTS' -import { Transaction, Transaction as dbTransaction } from '@entity/Transaction' +import { Transaction as dbTransaction } from '@entity/Transaction' import Decimal from 'decimal.js-light' import { GdtResolver } from './GdtResolver' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' @@ -25,7 +25,7 @@ export class BalanceResolver { const lastTransaction = context.lastTransaction ? context.lastTransaction - : await Transaction.findOne({ userId: user.id }, { order: { balanceDate: 'DESC' } }) + : await dbTransaction.findOne({ userId: user.id }, { order: { balanceDate: 'DESC' } }) // No balance found if (!lastTransaction) { From 633eef7c1615f16dc49e83b6eabd874c08a8ae66 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 29 Mar 2022 02:34:13 +0200 Subject: [PATCH 34/41] round GDD values in balance object --- backend/src/graphql/resolver/BalanceResolver.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/graphql/resolver/BalanceResolver.ts b/backend/src/graphql/resolver/BalanceResolver.ts index 7d30f6868..a311d5cdc 100644 --- a/backend/src/graphql/resolver/BalanceResolver.ts +++ b/backend/src/graphql/resolver/BalanceResolver.ts @@ -66,9 +66,9 @@ export class BalanceResolver { ) return new Balance({ - balance: calculatedDecay.balance, - decay: calculatedDecay.decay, - lastBookedBalance: lastTransaction.balance, + balance: calculatedDecay.balance.toDecimalPlaces(2, Decimal.ROUND_DOWN), // round towards zero + decay: calculatedDecay.decay.toDecimalPlaces(2, Decimal.ROUND_FLOOR), // round towards - infinity + lastBookedBalance: lastTransaction.balance.toDecimalPlaces(2, Decimal.ROUND_DOWN), balanceGDT, count, linkCount, From 8fd93ff8539bf404fe6c89d66b68df794ceba96e Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 29 Mar 2022 02:58:36 +0200 Subject: [PATCH 35/41] remove test for remove optin from DB after email verification --- backend/src/graphql/resolver/UserResolver.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 726ecfb09..c01cf2de9 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -221,10 +221,6 @@ describe('UserResolver', () => { expect(newUser[0].password).toEqual('3917921995996627700') }) - it('removes the optin', async () => { - await expect(LoginEmailOptIn.find()).resolves.toHaveLength(0) - }) - /* it('calls the klicktipp API', () => { expect(klicktippSignIn).toBeCalledWith( From 667b964d372c79e3afa1b6bd2292dd0a7e31844d Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 29 Mar 2022 02:59:25 +0200 Subject: [PATCH 36/41] coverage backend to 55% --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 18d1143db..ee602a343 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -528,7 +528,7 @@ jobs: report_name: Coverage Backend type: lcov result_path: ./backend/coverage/lcov.info - min_coverage: 54 + min_coverage: 55 token: ${{ github.token }} ########################################################################## From 90e6798a7d58e4257a2e70cb4c0594a32a6d9ee6 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 29 Mar 2022 16:10:06 +0200 Subject: [PATCH 37/41] rename context.count to context.transactionCount --- backend/src/graphql/resolver/BalanceResolver.ts | 4 ++-- backend/src/graphql/resolver/TransactionResolver.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/graphql/resolver/BalanceResolver.ts b/backend/src/graphql/resolver/BalanceResolver.ts index a311d5cdc..672e07b12 100644 --- a/backend/src/graphql/resolver/BalanceResolver.ts +++ b/backend/src/graphql/resolver/BalanceResolver.ts @@ -40,8 +40,8 @@ export class BalanceResolver { } const count = - context.count || context.count === 0 - ? context.count + context.transactionCount || context.transactionCount === 0 + ? context.transactionCount : await dbTransaction.count({ where: { userId: user.id } }) const linkCount = context.linkCount || context.linkCount === 0 diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index bb7a5c2f6..310ea37d1 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -170,7 +170,7 @@ export class TransactionResolver { offset, order, ) - context.count = userTransactionsCount + context.transactionCount = userTransactionsCount // find involved users; I am involved const involvedUserIds: number[] = [user.id] From b3002027cd34869d8b8dfb271ca2943fe2652dc0 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 29 Mar 2022 19:38:12 +0200 Subject: [PATCH 38/41] new backend version in env dist --- deployment/bare_metal/.env.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index 88dfff6f5..1cb78299a 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -18,7 +18,7 @@ WEBHOOK_GITHUB_SECRET=secret WEBHOOK_GITHUB_BRANCH=master # backend -BACKEND_CONFIG_VERSION=v1.2022-03-18 +BACKEND_CONFIG_VERSION=v2.2022-03-24 EMAIL=true EMAIL_USERNAME=peter@lustig.de From 8d1eff076544b8b359cf96878a727c9f767d48eb Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 29 Mar 2022 19:57:52 +0200 Subject: [PATCH 39/41] new config version --- backend/.env.dist | 2 +- backend/src/config/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/.env.dist b/backend/.env.dist index a81f0ad7c..447608533 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -1,4 +1,4 @@ -CONFIG_VERSION=v2.2022-03-24 +CONFIG_VERSION=v3.2022-03-29 # Server PORT=4000 diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 05be27298..50388d29a 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -14,7 +14,7 @@ const constants = { DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0 CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v2.2022-03-24', + EXPECTED: 'v3.2022-03-29', CURRENT: '', }, } From abd58ab9cf845ffe4a6aeaa16eefac676ce7c58d Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 29 Mar 2022 19:58:12 +0200 Subject: [PATCH 40/41] new config version --- deployment/bare_metal/.env.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index 1cb78299a..e46ee1d90 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -18,7 +18,7 @@ WEBHOOK_GITHUB_SECRET=secret WEBHOOK_GITHUB_BRANCH=master # backend -BACKEND_CONFIG_VERSION=v2.2022-03-24 +BACKEND_CONFIG_VERSION=v3.2022-03-29 EMAIL=true EMAIL_USERNAME=peter@lustig.de From 07ad22b71d5f21a998b3678fc5d73f0d89fc7d09 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 29 Mar 2022 20:10:00 +0200 Subject: [PATCH 41/41] change function name to print time duration --- backend/src/graphql/resolver/AdminResolver.ts | 4 +++- backend/src/graphql/resolver/UserResolver.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 9013a91f4..41d66e94e 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -39,8 +39,9 @@ import Paginated from '@arg/Paginated' import TransactionLinkFilters from '@arg/TransactionLinkFilters' import { Order } from '@enum/Order' import { communityUser } from '@/util/communityUser' -import { checkOptInCode, activationLink } from './UserResolver' +import { checkOptInCode, activationLink, printTimeDuration } from './UserResolver' import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' +import CONFIG from '@/config' // const EMAIL_OPT_IN_REGISTER = 1 // const EMAIL_OPT_UNKNOWN = 3 // elopage? @@ -397,6 +398,7 @@ export class AdminResolver { firstName: user.firstName, lastName: user.lastName, email, + duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME), }) /* uncomment this, when you need the activation link on the console diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index a3534e223..49959e3e7 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -369,7 +369,7 @@ export class UserResolver { firstName, lastName, email, - duration: printEmailCodeValidTime(), + duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME), }) /* uncomment this, when you need the activation link on the console @@ -409,7 +409,7 @@ export class UserResolver { firstName: user.firstName, lastName: user.lastName, email, - duration: printEmailCodeValidTime(), + duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME), }) /* uncomment this, when you need the activation link on the console