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 }}
##########################################################################
diff --git a/admin/src/pages/Creation.spec.js b/admin/src/pages/Creation.spec.js
index f9a4ed506..204c35817 100644
--- a/admin/src/pages/Creation.spec.js
+++ b/admin/src/pages/Creation.spec.js
@@ -71,6 +71,8 @@ describe('Creation', () => {
searchText: '',
currentPage: 1,
pageSize: 25,
+ isDeleted: false,
+ notActivated: false,
},
}),
)
@@ -269,6 +271,8 @@ describe('Creation', () => {
searchText: 'XX',
currentPage: 1,
pageSize: 25,
+ isDeleted: false,
+ notActivated: false,
},
}),
)
@@ -284,6 +288,8 @@ describe('Creation', () => {
searchText: '',
currentPage: 1,
pageSize: 25,
+ isDeleted: false,
+ notActivated: false,
},
}),
)
@@ -299,6 +305,8 @@ describe('Creation', () => {
searchText: '',
currentPage: 2,
pageSize: 25,
+ isDeleted: false,
+ notActivated: false,
},
}),
)
diff --git a/admin/src/pages/Creation.vue b/admin/src/pages/Creation.vue
index 8a229d7be..e5b93350f 100644
--- a/admin/src/pages/Creation.vue
+++ b/admin/src/pages/Creation.vue
@@ -102,6 +102,8 @@ export default {
searchText: this.criteria,
currentPage: this.currentPage,
pageSize: this.perPage,
+ notActivated: false,
+ isDeleted: false,
},
fetchPolicy: 'network-only',
})
diff --git a/admin/src/pages/UserSearch.spec.js b/admin/src/pages/UserSearch.spec.js
index bd18965ac..0b98d4d11 100644
--- a/admin/src/pages/UserSearch.spec.js
+++ b/admin/src/pages/UserSearch.spec.js
@@ -82,8 +82,8 @@ describe('UserSearch', () => {
searchText: '',
currentPage: 1,
pageSize: 25,
- notActivated: false,
- isDeleted: false,
+ notActivated: null,
+ isDeleted: null,
},
}),
)
@@ -102,7 +102,7 @@ describe('UserSearch', () => {
currentPage: 1,
pageSize: 25,
notActivated: true,
- isDeleted: false,
+ isDeleted: null,
},
}),
)
@@ -121,7 +121,7 @@ describe('UserSearch', () => {
searchText: '',
currentPage: 1,
pageSize: 25,
- notActivated: false,
+ notActivated: null,
isDeleted: true,
},
}),
@@ -141,8 +141,8 @@ describe('UserSearch', () => {
searchText: '',
currentPage: 2,
pageSize: 25,
- notActivated: false,
- isDeleted: false,
+ notActivated: null,
+ isDeleted: null,
},
}),
)
@@ -161,8 +161,8 @@ describe('UserSearch', () => {
searchText: 'search string',
currentPage: 1,
pageSize: 25,
- notActivated: false,
- isDeleted: false,
+ notActivated: null,
+ isDeleted: null,
},
}),
)
@@ -178,8 +178,8 @@ describe('UserSearch', () => {
searchText: '',
currentPage: 1,
pageSize: 25,
- notActivated: false,
- isDeleted: false,
+ notActivated: null,
+ isDeleted: null,
},
}),
)
diff --git a/admin/src/pages/UserSearch.vue b/admin/src/pages/UserSearch.vue
index 35d29ad7f..b2737bae6 100644
--- a/admin/src/pages/UserSearch.vue
+++ b/admin/src/pages/UserSearch.vue
@@ -3,11 +3,11 @@
- {{ filterCheckedEmails ? $t('all_emails') : $t('unregistered_emails') }}
+ {{ filterCheckedEmails ? $t('unregistered_emails') : $t('all_emails') }}
- {{ filterDeletedUser ? $t('all_emails') : $t('deleted_user') }}
+ {{ filterDeletedUser ? $t('deleted_user') : $t('all_emails') }}
@@ -60,8 +60,8 @@ export default {
searchResult: [],
massCreation: [],
criteria: '',
- filterCheckedEmails: false,
- filterDeletedUser: false,
+ filterCheckedEmails: null,
+ filterDeletedUser: null,
rows: 0,
currentPage: 1,
perPage: 25,
@@ -70,11 +70,11 @@ export default {
},
methods: {
unconfirmedRegisterMails() {
- this.filterCheckedEmails = !this.filterCheckedEmails
+ this.filterCheckedEmails = this.filterCheckedEmails ? null : true
this.getUsers()
},
deletedUserSearch() {
- this.filterDeletedUser = !this.filterDeletedUser
+ this.filterDeletedUser = this.filterDeletedUser ? null : true
this.getUsers()
},
getUsers() {
diff --git a/backend/.env.dist b/backend/.env.dist
index b8a3d5dbe..447608533 100644
--- a/backend/.env.dist
+++ b/backend/.env.dist
@@ -1,4 +1,4 @@
-CONFIG_VERSION=v1.2022-03-18
+CONFIG_VERSION=v3.2022-03-29
# Server
PORT=4000
@@ -41,8 +41,10 @@ EMAIL_PASSWORD=xxx
EMAIL_SMTP_URL=gmail.com
EMAIL_SMTP_PORT=587
EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{optin}{code}
-EMAIL_LINK_SETPASSWORD=http://localhost/reset/{optin}
-EMAIL_CODE_VALID_TIME=10
+EMAIL_LINK_SETPASSWORD=http://localhost/reset/{code}
+EMAIL_LINK_FORGOTPASSWORD=http://localhost/forgot-password
+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/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts
index e68bf0a6f..8188b3daa 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_BALANCE = 'GDT_BALANCE',
// 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..891fe1844 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_BALANCE,
])
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts
index dcfea2bdb..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: 'v1.2022-03-18',
+ EXPECTED: 'v3.2022-03-29',
CURRENT: '',
},
}
@@ -70,8 +70,15 @@ 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}',
+ EMAIL_LINK_FORGOTPASSWORD:
+ process.env.EMAIL_LINK_FORGOTPASSWORD || 'http://localhost/forgot-password',
+ // 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) || 10
+ ? 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,
}
diff --git a/backend/src/graphql/arg/SearchUsersArgs.ts b/backend/src/graphql/arg/SearchUsersArgs.ts
index 52da77852..2a94d8998 100644
--- a/backend/src/graphql/arg/SearchUsersArgs.ts
+++ b/backend/src/graphql/arg/SearchUsersArgs.ts
@@ -12,8 +12,8 @@ export default class SearchUsersArgs {
pageSize?: number
@Field(() => Boolean, { nullable: true })
- notActivated?: boolean
+ notActivated?: boolean | null
@Field(() => Boolean, { nullable: true })
- isDeleted?: boolean
+ isDeleted?: boolean | null
}
diff --git a/backend/src/graphql/enum/OptInType.ts b/backend/src/graphql/enum/OptInType.ts
new file mode 100644
index 000000000..2dd2d07b0
--- /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/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
}
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/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts
index 283a4ed5a..1ed0422ef 100644
--- a/backend/src/graphql/resolver/AdminResolver.ts
+++ b/backend/src/graphql/resolver/AdminResolver.ts
@@ -39,6 +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, printTimeDuration } from './UserResolver'
+import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
+import CONFIG from '@/config'
// const EMAIL_OPT_IN_REGISTER = 1
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
@@ -55,19 +58,19 @@ export class AdminResolver {
searchText,
currentPage = 1,
pageSize = 25,
- notActivated = false,
- isDeleted = false,
+ notActivated = null,
+ isDeleted = null,
}: SearchUsersArgs,
): Promise {
const userRepository = getCustomRepository(UserRepository)
const filterCriteria: ObjectLiteral[] = []
- if (notActivated) {
- filterCriteria.push({ emailChecked: false })
+ if (notActivated !== null) {
+ filterCriteria.push({ emailChecked: !notActivated })
}
- if (isDeleted) {
- filterCriteria.push({ deletedAt: Not(IsNull()) })
+ if (isDeleted !== null) {
+ filterCriteria.push({ deletedAt: isDeleted ? Not(IsNull()) : IsNull() })
}
const userFields = ['id', 'firstName', 'lastName', 'email', 'emailChecked', 'deletedAt']
@@ -375,6 +378,40 @@ export class AdminResolver {
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({
+ where: { userId: user.id },
+ order: { updatedAt: 'DESC' },
+ })
+
+ optInCode = await checkOptInCode(optInCode, user.id)
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const emailSent = await sendAccountActivationEmail({
+ link: activationLink(optInCode),
+ 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
+ // 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
+ }
+
@Authorized([RIGHTS.LIST_TRANSACTION_LINKS_ADMIN])
@Query(() => TransactionLinkResult)
async listTransactionLinksAdmin(
diff --git a/backend/src/graphql/resolver/BalanceResolver.ts b/backend/src/graphql/resolver/BalanceResolver.ts
index 09d2fdc92..672e07b12 100644
--- a/backend/src/graphql/resolver/BalanceResolver.ts
+++ b/backend/src/graphql/resolver/BalanceResolver.ts
@@ -5,36 +5,74 @@ 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 as dbTransaction } from '@entity/Transaction'
import Decimal from 'decimal.js-light'
+import { GdtResolver } from './GdtResolver'
+import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
+import { MoreThan, getCustomRepository } from '@dbTools/typeorm'
+import { TransactionLinkRepository } from '@repository/TransactionLink'
@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 lastTransaction = await Transaction.findOne(
- { userId: user.id },
- { order: { balanceDate: 'DESC' } },
- )
+ const gdtResolver = new GdtResolver()
+ const balanceGDT = await gdtResolver.gdtBalance(context)
+
+ const lastTransaction = context.lastTransaction
+ ? context.lastTransaction
+ : await dbTransaction.findOne({ userId: user.id }, { order: { balanceDate: 'DESC' } })
// No balance found
if (!lastTransaction) {
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 =
+ context.transactionCount || context.transactionCount === 0
+ ? context.transactionCount
+ : 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 } = context.sumHoldAvailableAmount
+ ? { sumHoldAvailableAmount: context.sumHoldAvailableAmount }
+ : await transactionLinkRepository.summary(user.id, now)
+
+ const calculatedDecay = calculateDecay(
+ lastTransaction.balance.minus(sumHoldAvailableAmount.toString()),
+ lastTransaction.balanceDate,
+ now,
+ )
+
return new Balance({
- balance: lastTransaction.balance,
- decay: calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance,
- decay_date: now.toString(),
+ 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,
+ lastBookedDate: lastTransaction.balanceDate,
})
}
}
diff --git a/backend/src/graphql/resolver/GdtResolver.ts b/backend/src/graphql/resolver/GdtResolver.ts
index 26ae9b210..e2409160b 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'
@@ -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_BALANCE])
+ @Query(() => Number)
+ async gdtBalance(@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
diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts
index 0ff43486c..310ea37d1 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'
@@ -32,7 +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 { BalanceResolver } from './BalanceResolver'
const MEMO_MAX_CHARS = 255
const MEMO_MIN_CHARS = 5
@@ -154,23 +153,11 @@ export class TransactionResolver {
{ order: { balanceDate: 'DESC' } },
)
- // 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)
- }
+ 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
@@ -183,6 +170,7 @@ export class TransactionResolver {
offset,
order,
)
+ context.transactionCount = userTransactionsCount
// find involved users; I am involved
const involvedUserIds: number[] = [user.id]
@@ -205,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) {
@@ -237,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])
diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts
index 53f39668e..07b8e59e2 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 { printTimeDuration } from './UserResolver'
// import { klicktippSignIn } from '@/apis/KlicktippController'
@@ -133,6 +134,7 @@ describe('UserResolver', () => {
firstName: 'Peter',
lastName: 'Lustig',
email: 'peter@lustig.de',
+ duration: expect.any(String),
})
})
})
@@ -220,10 +222,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(
@@ -415,3 +413,17 @@ describe('UserResolver', () => {
})
})
})
+
+describe('printTimeDuration', () => {
+ it('works with 10 minutes', () => {
+ expect(printTimeDuration(10)).toBe('10 minutes')
+ })
+
+ it('works with 1440 minutes', () => {
+ expect(printTimeDuration(1440)).toBe('24 hours')
+ })
+
+ it('works with 1410 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 b24aa1b58..49959e3e7 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'
@@ -15,8 +15,9 @@ 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 { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
import { klicktippSignIn } from '@/apis/KlicktippController'
import { RIGHTS } from '@/auth/RIGHTS'
@@ -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,57 +146,47 @@ 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,
- })
- if (emailOptIn) {
- if (isOptInCodeValid(emailOptIn)) {
- throw new Error(`email already sent less than $(CONFIG.EMAIL_CODE_VALID_TIME} minutes ago`)
- }
- emailOptIn.updatedAt = new Date()
- emailOptIn.resendCount++
- } else {
- emailOptIn = new LoginEmailOptIn()
- emailOptIn.verificationCode = random(64)
- emailOptIn.userId = loginUserId
- emailOptIn.emailOptInTypeId = EMAIL_OPT_IN_REGISTER
- }
- 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 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 getOptInCode = async (loginUserId: number): Promise => {
- let optInCode = await LoginEmailOptIn.findOne({
- userId: loginUserId,
- emailOptInTypeId: EMAIL_OPT_IN_RESET_PASSWORD,
- })
-
- // Check for `CONFIG.EMAIL_CODE_VALID_TIME` minute delay
+// needed by AdminResolver
+// checks if given code exists and can be resent
+// if optIn does not exits, it is created
+export const checkOptInCode = async (
+ optInCode: LoginEmailOptIn | undefined,
+ userId: number,
+ optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER,
+): Promise => {
if (optInCode) {
- if (isOptInCodeValid(optInCode)) {
- throw new Error(`email already sent less than $(CONFIG.EMAIL_CODE_VALID_TIME} minutes ago`)
+ 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 = new LoginEmailOptIn()
- optInCode.verificationCode = random(64)
- optInCode.userId = loginUserId
- optInCode.emailOptInTypeId = EMAIL_OPT_IN_RESET_PASSWORD
+ optInCode = newEmailOptIn(userId)
}
- await LoginEmailOptIn.save(optInCode)
+ optInCode.emailOptInTypeId = optInType
+ await LoginEmailOptIn.save(optInCode).catch(() => {
+ throw new Error('Unable to save optin code.')
+ })
return optInCode
}
+export const activationLink = (optInCode: LoginEmailOptIn): string => {
+ return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, optInCode.verificationCode.toString())
+}
+
@Resolver()
export class UserResolver {
@Authorized([RIGHTS.VERIFY_LOGIN])
@@ -363,9 +351,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,
@@ -378,6 +369,7 @@ export class UserResolver {
firstName,
lastName,
email,
+ duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
})
/* uncomment this, when you need the activation link on the console
@@ -398,70 +390,26 @@ 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 {
- // 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,
+ })
- const link = CONFIG.EMAIL_LINK_SETPASSWORD.replace(
- /{optin}/g,
- optInCode.verificationCode.toString(),
- )
+ 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({
- link,
+ const emailSent = await sendResetPasswordEmailMailer({
+ link: activationLink(optInCode),
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
@@ -494,8 +442,10 @@ 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`)
+ if (!isOptInValid(optInCode)) {
+ throw new Error(
+ `email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
+ )
}
// load user
@@ -538,11 +488,6 @@ export class UserResolver {
throw new Error('error saving user: ' + error)
})
- // Delete Code
- await queryRunner.manager.remove(optInCode).catch((error) => {
- throw new Error('error deleting code: ' + error)
- })
-
await queryRunner.commitTransaction()
} catch (e) {
await queryRunner.rollbackTransaction()
@@ -553,7 +498,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 {
@@ -573,8 +518,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 $(CONFIG.EMAIL_CODE_VALID_TIME} minutes ago`)
+ if (!isOptInValid(optInCode)) {
+ throw new Error(
+ `email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
+ )
}
return true
}
@@ -680,7 +627,34 @@ 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 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 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(time / 60),
+ minutes: time % 60,
+ }
+ }
+ return { minutes: time }
+}
+
+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
}
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/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/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'),
})
})
})
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 bf5d1a2e9..2755c4c0a 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.
@@ -10,6 +17,15 @@ Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradi
${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`,
},
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`,
},
diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist
index ee0ac3cc3..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=v1.2022-03-18
+BACKEND_CONFIG_VERSION=v3.2022-03-29
EMAIL=true
EMAIL_USERNAME=peter@lustig.de
@@ -49,6 +49,8 @@ KLICKTIPP_APIKEY_EN=
DATABASE_CONFIG_VERSION=v1.2022-03-18
# frontend
+FRONTEND_CONFIG_VERSION=v1.2022-03-18
+
GRAPHQL_URI=https://stage1.gradido.net/graphql
ADMIN_AUTH_URL=https://stage1.gradido.net/admin/authenticate?token={token}
@@ -62,5 +64,7 @@ META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Econo
META_AUTHOR="Bernd Hückstädt - Gradido-Akademie"
# admin
+ADMIN_CONFIG_VERSION=v1.2022-03-18
+
WALLET_AUTH_URL=https://stage1.gradido.net/authenticate?token={token}
WALLET_URL=https://stage1.gradido.net/login
\ No newline at end of file
diff --git a/deployment/bare_metal/setup.md b/deployment/bare_metal/setup.md
index 55f657df2..f39228879 100644
--- a/deployment/bare_metal/setup.md
+++ b/deployment/bare_metal/setup.md
@@ -78,8 +78,8 @@
> git clone https://github.com/gradido/gradido.git
# Timezone
-# Note: This is not needed - UTC(default) is REQUIRED for production data
-# > sudo timedatectl set-timezone UTC
+# Note: This is needed - since there is Summer-Time included in the default server Setup - UTC is REQUIRED for production data
+> sudo timedatectl set-timezone UTC
# > sudo timedatectl set-ntp on
# > sudo apt purge ntp
# > sudo systemctl start systemd-timesyncd
diff --git a/deployment/bare_metal/start.sh b/deployment/bare_metal/start.sh
index 250971419..03ba87445 100755
--- a/deployment/bare_metal/start.sh
+++ b/deployment/bare_metal/start.sh
@@ -67,7 +67,7 @@ BRANCH=${1:-master}
echo "Starting with git pull - branch:$BRANCH" >> $UPDATE_HTML
cd $PROJECT_ROOT
# TODO: this overfetches alot, but ensures we can use start.sh with tags
-git fetch origin --all
+git fetch --all
git checkout $BRANCH
git pull
export BUILD_COMMIT="$(git rev-parse HEAD)"
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.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'],
},
},
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) => {
diff --git a/frontend/src/pages/ResetPassword.spec.js b/frontend/src/pages/ResetPassword.spec.js
index c43f71932..36efa0a11 100644
--- a/frontend/src/pages/ResetPassword.spec.js
+++ b/frontend/src/pages/ResetPassword.spec.js
@@ -150,13 +150,18 @@ describe('ResetPassword', () => {
describe('server response with error code > 10min', () => {
beforeEach(async () => {
- apolloMutationMock.mockRejectedValue({ message: '...Code is older than 10 minutes' })
+ jest.clearAllMocks()
+ 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 7771be5f6..a737246bc 100644
--- a/frontend/src/pages/ResetPassword.vue
+++ b/frontend/src/pages/ResetPassword.vue
@@ -108,7 +108,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')
})
},