mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into mass_creation_do_not_show_unactivated
This commit is contained in:
commit
10ae00dba8
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -528,7 +528,7 @@ jobs:
|
|||||||
report_name: Coverage Backend
|
report_name: Coverage Backend
|
||||||
type: lcov
|
type: lcov
|
||||||
result_path: ./backend/coverage/lcov.info
|
result_path: ./backend/coverage/lcov.info
|
||||||
min_coverage: 54
|
min_coverage: 55
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
CONFIG_VERSION=v1.2022-03-18
|
CONFIG_VERSION=v3.2022-03-29
|
||||||
|
|
||||||
# Server
|
# Server
|
||||||
PORT=4000
|
PORT=4000
|
||||||
@ -41,8 +41,10 @@ EMAIL_PASSWORD=xxx
|
|||||||
EMAIL_SMTP_URL=gmail.com
|
EMAIL_SMTP_URL=gmail.com
|
||||||
EMAIL_SMTP_PORT=587
|
EMAIL_SMTP_PORT=587
|
||||||
EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{optin}{code}
|
EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{optin}{code}
|
||||||
EMAIL_LINK_SETPASSWORD=http://localhost/reset/{optin}
|
EMAIL_LINK_SETPASSWORD=http://localhost/reset/{code}
|
||||||
EMAIL_CODE_VALID_TIME=10
|
EMAIL_LINK_FORGOTPASSWORD=http://localhost/forgot-password
|
||||||
|
EMAIL_CODE_VALID_TIME=1440
|
||||||
|
EMAIL_CODE_REQUEST_TIME=10
|
||||||
|
|
||||||
# Webhook
|
# Webhook
|
||||||
WEBHOOK_ELOPAGE_SECRET=secret
|
WEBHOOK_ELOPAGE_SECRET=secret
|
||||||
@ -24,6 +24,7 @@ export enum RIGHTS {
|
|||||||
QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK',
|
QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK',
|
||||||
REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK',
|
REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK',
|
||||||
LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS',
|
LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS',
|
||||||
|
GDT_BALANCE = 'GDT_BALANCE',
|
||||||
// Admin
|
// Admin
|
||||||
SEARCH_USERS = 'SEARCH_USERS',
|
SEARCH_USERS = 'SEARCH_USERS',
|
||||||
CREATE_PENDING_CREATION = 'CREATE_PENDING_CREATION',
|
CREATE_PENDING_CREATION = 'CREATE_PENDING_CREATION',
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export const ROLE_USER = new Role('user', [
|
|||||||
RIGHTS.DELETE_TRANSACTION_LINK,
|
RIGHTS.DELETE_TRANSACTION_LINK,
|
||||||
RIGHTS.REDEEM_TRANSACTION_LINK,
|
RIGHTS.REDEEM_TRANSACTION_LINK,
|
||||||
RIGHTS.LIST_TRANSACTION_LINKS,
|
RIGHTS.LIST_TRANSACTION_LINKS,
|
||||||
|
RIGHTS.GDT_BALANCE,
|
||||||
])
|
])
|
||||||
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
|
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ const constants = {
|
|||||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0
|
DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0
|
||||||
CONFIG_VERSION: {
|
CONFIG_VERSION: {
|
||||||
DEFAULT: 'DEFAULT',
|
DEFAULT: 'DEFAULT',
|
||||||
EXPECTED: 'v1.2022-03-18',
|
EXPECTED: 'v3.2022-03-29',
|
||||||
CURRENT: '',
|
CURRENT: '',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -70,8 +70,15 @@ const email = {
|
|||||||
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{optin}{code}',
|
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{optin}{code}',
|
||||||
EMAIL_LINK_SETPASSWORD:
|
EMAIL_LINK_SETPASSWORD:
|
||||||
process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{optin}',
|
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
|
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,
|
: 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
backend/src/graphql/enum/OptInType.ts
Normal file
11
backend/src/graphql/enum/OptInType.ts
Normal file
@ -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
|
||||||
|
})
|
||||||
@ -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 { ObjectType, Field } from 'type-graphql'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class Balance {
|
export class Balance {
|
||||||
constructor(json: any) {
|
constructor(data: {
|
||||||
this.balance = json.balance
|
balance: Decimal
|
||||||
this.decay = json.decay
|
decay: Decimal
|
||||||
this.decayDate = json.decay_date
|
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)
|
@Field(() => Decimal)
|
||||||
balance: Decimal
|
balance: Decimal
|
||||||
|
|
||||||
|
// the decay since the last booked balance
|
||||||
@Field(() => Decimal)
|
@Field(() => Decimal)
|
||||||
decay: 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)
|
@Field(() => Date)
|
||||||
decayDate: Date
|
decayStartBlock: Date
|
||||||
|
|
||||||
|
// may be null as there may be no transaction
|
||||||
|
@Field(() => Date, { nullable: true })
|
||||||
|
lastBookedDate: Date | null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,40 +1,16 @@
|
|||||||
import { ObjectType, Field } from 'type-graphql'
|
import { ObjectType, Field } from 'type-graphql'
|
||||||
import CONFIG from '@/config'
|
|
||||||
import Decimal from 'decimal.js-light'
|
|
||||||
import { Transaction } from './Transaction'
|
import { Transaction } from './Transaction'
|
||||||
|
import { Balance } from './Balance'
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class TransactionList {
|
export class TransactionList {
|
||||||
constructor(
|
constructor(balance: Balance, transactions: Transaction[]) {
|
||||||
balance: Decimal,
|
|
||||||
transactions: Transaction[],
|
|
||||||
count: number,
|
|
||||||
linkCount: number,
|
|
||||||
balanceGDT?: number | null,
|
|
||||||
decayStartBlock: Date = CONFIG.DECAY_START_TIME,
|
|
||||||
) {
|
|
||||||
this.balance = balance
|
this.balance = balance
|
||||||
this.transactions = transactions
|
this.transactions = transactions
|
||||||
this.count = count
|
|
||||||
this.linkCount = linkCount
|
|
||||||
this.balanceGDT = balanceGDT || null
|
|
||||||
this.decayStartBlock = decayStartBlock
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Field(() => Number, { nullable: true })
|
@Field(() => Balance)
|
||||||
balanceGDT: number | null
|
balance: Balance
|
||||||
|
|
||||||
@Field(() => Number)
|
|
||||||
count: number
|
|
||||||
|
|
||||||
@Field(() => Number)
|
|
||||||
linkCount: number
|
|
||||||
|
|
||||||
@Field(() => Decimal)
|
|
||||||
balance: Decimal
|
|
||||||
|
|
||||||
@Field(() => Date)
|
|
||||||
decayStartBlock: Date
|
|
||||||
|
|
||||||
@Field(() => [Transaction])
|
@Field(() => [Transaction])
|
||||||
transactions: Transaction[]
|
transactions: Transaction[]
|
||||||
|
|||||||
@ -39,6 +39,9 @@ import Paginated from '@arg/Paginated'
|
|||||||
import TransactionLinkFilters from '@arg/TransactionLinkFilters'
|
import TransactionLinkFilters from '@arg/TransactionLinkFilters'
|
||||||
import { Order } from '@enum/Order'
|
import { Order } from '@enum/Order'
|
||||||
import { communityUser } from '@/util/communityUser'
|
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_IN_REGISTER = 1
|
||||||
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
||||||
@ -375,6 +378,40 @@ export class AdminResolver {
|
|||||||
return userTransactions.map((t) => new Transaction(t, new User(user), communityUser))
|
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<boolean> {
|
||||||
|
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])
|
@Authorized([RIGHTS.LIST_TRANSACTION_LINKS_ADMIN])
|
||||||
@Query(() => TransactionLinkResult)
|
@Query(() => TransactionLinkResult)
|
||||||
async listTransactionLinksAdmin(
|
async listTransactionLinksAdmin(
|
||||||
|
|||||||
@ -5,36 +5,74 @@ import { Resolver, Query, Ctx, Authorized } from 'type-graphql'
|
|||||||
import { Balance } from '@model/Balance'
|
import { Balance } from '@model/Balance'
|
||||||
import { calculateDecay } from '@/util/decay'
|
import { calculateDecay } from '@/util/decay'
|
||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
import { Transaction } from '@entity/Transaction'
|
import { Transaction as dbTransaction } from '@entity/Transaction'
|
||||||
import Decimal from 'decimal.js-light'
|
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()
|
@Resolver()
|
||||||
export class BalanceResolver {
|
export class BalanceResolver {
|
||||||
@Authorized([RIGHTS.BALANCE])
|
@Authorized([RIGHTS.BALANCE])
|
||||||
@Query(() => Balance)
|
@Query(() => Balance)
|
||||||
async balance(@Ctx() context: any): Promise<Balance> {
|
async balance(@Ctx() context: any): Promise<Balance> {
|
||||||
// load user and balance
|
|
||||||
const { user } = context
|
const { user } = context
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|
||||||
const lastTransaction = await Transaction.findOne(
|
const gdtResolver = new GdtResolver()
|
||||||
{ userId: user.id },
|
const balanceGDT = await gdtResolver.gdtBalance(context)
|
||||||
{ order: { balanceDate: 'DESC' } },
|
|
||||||
)
|
const lastTransaction = context.lastTransaction
|
||||||
|
? context.lastTransaction
|
||||||
|
: await dbTransaction.findOne({ userId: user.id }, { order: { balanceDate: 'DESC' } })
|
||||||
|
|
||||||
// No balance found
|
// No balance found
|
||||||
if (!lastTransaction) {
|
if (!lastTransaction) {
|
||||||
return new Balance({
|
return new Balance({
|
||||||
balance: new Decimal(0),
|
balance: new Decimal(0),
|
||||||
decay: 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({
|
return new Balance({
|
||||||
balance: lastTransaction.balance,
|
balance: calculatedDecay.balance.toDecimalPlaces(2, Decimal.ROUND_DOWN), // round towards zero
|
||||||
decay: calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance,
|
decay: calculatedDecay.decay.toDecimalPlaces(2, Decimal.ROUND_FLOOR), // round towards - infinity
|
||||||
decay_date: now.toString(),
|
lastBookedBalance: lastTransaction.balance.toDecimalPlaces(2, Decimal.ROUND_DOWN),
|
||||||
|
balanceGDT,
|
||||||
|
count,
|
||||||
|
linkCount,
|
||||||
|
lastBookedDate: lastTransaction.balanceDate,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { Resolver, Query, Args, Ctx, Authorized, Arg } from 'type-graphql'
|
|||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
import { GdtEntryList } from '@model/GdtEntryList'
|
import { GdtEntryList } from '@model/GdtEntryList'
|
||||||
import Paginated from '@arg/Paginated'
|
import Paginated from '@arg/Paginated'
|
||||||
import { apiGet } from '@/apis/HttpRequest'
|
import { apiGet, apiPost } from '@/apis/HttpRequest'
|
||||||
import { Order } from '@enum/Order'
|
import { Order } from '@enum/Order'
|
||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
|
|
||||||
@ -13,13 +13,11 @@ import { RIGHTS } from '@/auth/RIGHTS'
|
|||||||
export class GdtResolver {
|
export class GdtResolver {
|
||||||
@Authorized([RIGHTS.LIST_GDT_ENTRIES])
|
@Authorized([RIGHTS.LIST_GDT_ENTRIES])
|
||||||
@Query(() => GdtEntryList)
|
@Query(() => GdtEntryList)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
async listGDTEntries(
|
async listGDTEntries(
|
||||||
@Args()
|
@Args()
|
||||||
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
||||||
@Ctx() context: any,
|
@Ctx() context: any,
|
||||||
): Promise<GdtEntryList> {
|
): Promise<GdtEntryList> {
|
||||||
// load user
|
|
||||||
const userEntity = context.user
|
const userEntity = context.user
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -35,6 +33,25 @@ export class GdtResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Authorized([RIGHTS.GDT_BALANCE])
|
||||||
|
@Query(() => Number)
|
||||||
|
async gdtBalance(@Ctx() context: any): Promise<number | null> {
|
||||||
|
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])
|
@Authorized([RIGHTS.EXIST_PID])
|
||||||
@Query(() => Number)
|
@Query(() => Number)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|||||||
@ -6,7 +6,6 @@
|
|||||||
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
|
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
|
||||||
import { getCustomRepository, getConnection } from '@dbTools/typeorm'
|
import { getCustomRepository, getConnection } from '@dbTools/typeorm'
|
||||||
|
|
||||||
import CONFIG from '@/config'
|
|
||||||
import { sendTransactionReceivedEmail } from '@/mailer/sendTransactionReceivedEmail'
|
import { sendTransactionReceivedEmail } from '@/mailer/sendTransactionReceivedEmail'
|
||||||
|
|
||||||
import { Transaction } from '@model/Transaction'
|
import { Transaction } from '@model/Transaction'
|
||||||
@ -24,7 +23,6 @@ import { User as dbUser } from '@entity/User'
|
|||||||
import { Transaction as dbTransaction } from '@entity/Transaction'
|
import { Transaction as dbTransaction } from '@entity/Transaction'
|
||||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||||
|
|
||||||
import { apiPost } from '@/apis/HttpRequest'
|
|
||||||
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||||
import { calculateBalance, isHexPublicKey } from '@/util/validate'
|
import { calculateBalance, isHexPublicKey } from '@/util/validate'
|
||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
@ -32,7 +30,8 @@ import { User } from '@model/User'
|
|||||||
import { communityUser } from '@/util/communityUser'
|
import { communityUser } from '@/util/communityUser'
|
||||||
import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions'
|
import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
import { calculateDecay } from '@/util/decay'
|
|
||||||
|
import { BalanceResolver } from './BalanceResolver'
|
||||||
|
|
||||||
const MEMO_MAX_CHARS = 255
|
const MEMO_MAX_CHARS = 255
|
||||||
const MEMO_MIN_CHARS = 5
|
const MEMO_MIN_CHARS = 5
|
||||||
@ -154,23 +153,11 @@ export class TransactionResolver {
|
|||||||
{ order: { balanceDate: 'DESC' } },
|
{ order: { balanceDate: 'DESC' } },
|
||||||
)
|
)
|
||||||
|
|
||||||
// get GDT
|
const balanceResolver = new BalanceResolver()
|
||||||
let balanceGDT = null
|
context.lastTransaction = lastTransaction
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!lastTransaction) {
|
if (!lastTransaction) {
|
||||||
return new TransactionList(new Decimal(0), [], 0, 0, balanceGDT)
|
return new TransactionList(await balanceResolver.balance(context), [])
|
||||||
}
|
}
|
||||||
|
|
||||||
// find transactions
|
// find transactions
|
||||||
@ -183,6 +170,7 @@ export class TransactionResolver {
|
|||||||
offset,
|
offset,
|
||||||
order,
|
order,
|
||||||
)
|
)
|
||||||
|
context.transactionCount = userTransactionsCount
|
||||||
|
|
||||||
// find involved users; I am involved
|
// find involved users; I am involved
|
||||||
const involvedUserIds: number[] = [user.id]
|
const involvedUserIds: number[] = [user.id]
|
||||||
@ -205,6 +193,8 @@ export class TransactionResolver {
|
|||||||
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
|
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
|
||||||
const { sumHoldAvailableAmount, sumAmount, lastDate, firstDate, transactionLinkcount } =
|
const { sumHoldAvailableAmount, sumAmount, lastDate, firstDate, transactionLinkcount } =
|
||||||
await transactionLinkRepository.summary(user.id, now)
|
await transactionLinkRepository.summary(user.id, now)
|
||||||
|
context.linkCount = transactionLinkcount
|
||||||
|
context.sumHoldAvailableAmount = sumHoldAvailableAmount
|
||||||
|
|
||||||
// decay & link transactions
|
// decay & link transactions
|
||||||
if (currentPage === 1 && order === Order.DESC) {
|
if (currentPage === 1 && order === Order.DESC) {
|
||||||
@ -237,15 +227,7 @@ export class TransactionResolver {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Construct Result
|
// Construct Result
|
||||||
return new TransactionList(
|
return new TransactionList(await balanceResolver.balance(context), transactions)
|
||||||
calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance.minus(
|
|
||||||
sumHoldAvailableAmount.toString(),
|
|
||||||
),
|
|
||||||
transactions,
|
|
||||||
userTransactionsCount,
|
|
||||||
transactionLinkcount,
|
|
||||||
balanceGDT,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.SEND_COINS])
|
@Authorized([RIGHTS.SEND_COINS])
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
|||||||
import { User } from '@entity/User'
|
import { User } from '@entity/User'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
|
import { printTimeDuration } from './UserResolver'
|
||||||
|
|
||||||
// import { klicktippSignIn } from '@/apis/KlicktippController'
|
// import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||||
|
|
||||||
@ -133,6 +134,7 @@ describe('UserResolver', () => {
|
|||||||
firstName: 'Peter',
|
firstName: 'Peter',
|
||||||
lastName: 'Lustig',
|
lastName: 'Lustig',
|
||||||
email: 'peter@lustig.de',
|
email: 'peter@lustig.de',
|
||||||
|
duration: expect.any(String),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -220,10 +222,6 @@ describe('UserResolver', () => {
|
|||||||
expect(newUser[0].password).toEqual('3917921995996627700')
|
expect(newUser[0].password).toEqual('3917921995996627700')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('removes the optin', async () => {
|
|
||||||
await expect(LoginEmailOptIn.find()).resolves.toHaveLength(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
it('calls the klicktipp API', () => {
|
it('calls the klicktipp API', () => {
|
||||||
expect(klicktippSignIn).toBeCalledWith(
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
|
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 CONFIG from '@/config'
|
||||||
import { User } from '@model/User'
|
import { User } from '@model/User'
|
||||||
import { User as DbUser } from '@entity/User'
|
import { User as DbUser } from '@entity/User'
|
||||||
@ -15,8 +15,9 @@ import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs'
|
|||||||
import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware'
|
import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware'
|
||||||
import { UserSettingRepository } from '@repository/UserSettingRepository'
|
import { UserSettingRepository } from '@repository/UserSettingRepository'
|
||||||
import { Setting } from '@enum/Setting'
|
import { Setting } from '@enum/Setting'
|
||||||
|
import { OptInType } from '@enum/OptInType'
|
||||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||||
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
|
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
|
||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
import { klicktippSignIn } from '@/apis/KlicktippController'
|
import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
@ -24,9 +25,6 @@ import { ROLE_ADMIN } from '@/auth/ROLES'
|
|||||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||||
import { ServerUser } from '@entity/ServerUser'
|
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
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const sodium = require('sodium-native')
|
const sodium = require('sodium-native')
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
@ -148,57 +146,47 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B
|
|||||||
|
|
||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
const createEmailOptIn = async (
|
|
||||||
loginUserId: number,
|
const newEmailOptIn = (userId: number): LoginEmailOptIn => {
|
||||||
queryRunner: QueryRunner,
|
const emailOptIn = new LoginEmailOptIn()
|
||||||
): Promise<LoginEmailOptIn> => {
|
emailOptIn.verificationCode = random(64)
|
||||||
let emailOptIn = await LoginEmailOptIn.findOne({
|
emailOptIn.userId = userId
|
||||||
userId: loginUserId,
|
emailOptIn.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER
|
||||||
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')
|
|
||||||
})
|
|
||||||
return emailOptIn
|
return emailOptIn
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOptInCode = async (loginUserId: number): Promise<LoginEmailOptIn> => {
|
// needed by AdminResolver
|
||||||
let optInCode = await LoginEmailOptIn.findOne({
|
// checks if given code exists and can be resent
|
||||||
userId: loginUserId,
|
// if optIn does not exits, it is created
|
||||||
emailOptInTypeId: EMAIL_OPT_IN_RESET_PASSWORD,
|
export const checkOptInCode = async (
|
||||||
})
|
optInCode: LoginEmailOptIn | undefined,
|
||||||
|
userId: number,
|
||||||
// Check for `CONFIG.EMAIL_CODE_VALID_TIME` minute delay
|
optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER,
|
||||||
|
): Promise<LoginEmailOptIn> => {
|
||||||
if (optInCode) {
|
if (optInCode) {
|
||||||
if (isOptInCodeValid(optInCode)) {
|
if (!canResendOptIn(optInCode)) {
|
||||||
throw new Error(`email already sent less than $(CONFIG.EMAIL_CODE_VALID_TIME} minutes ago`)
|
throw new Error(
|
||||||
|
`email already sent less than ${printTimeDuration(
|
||||||
|
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||||
|
)} minutes ago`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
optInCode.updatedAt = new Date()
|
optInCode.updatedAt = new Date()
|
||||||
optInCode.resendCount++
|
optInCode.resendCount++
|
||||||
} else {
|
} else {
|
||||||
optInCode = new LoginEmailOptIn()
|
optInCode = newEmailOptIn(userId)
|
||||||
optInCode.verificationCode = random(64)
|
|
||||||
optInCode.userId = loginUserId
|
|
||||||
optInCode.emailOptInTypeId = EMAIL_OPT_IN_RESET_PASSWORD
|
|
||||||
}
|
}
|
||||||
await LoginEmailOptIn.save(optInCode)
|
optInCode.emailOptInTypeId = optInType
|
||||||
|
await LoginEmailOptIn.save(optInCode).catch(() => {
|
||||||
|
throw new Error('Unable to save optin code.')
|
||||||
|
})
|
||||||
return optInCode
|
return optInCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const activationLink = (optInCode: LoginEmailOptIn): string => {
|
||||||
|
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, optInCode.verificationCode.toString())
|
||||||
|
}
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class UserResolver {
|
export class UserResolver {
|
||||||
@Authorized([RIGHTS.VERIFY_LOGIN])
|
@Authorized([RIGHTS.VERIFY_LOGIN])
|
||||||
@ -363,9 +351,12 @@ export class UserResolver {
|
|||||||
throw new Error('error saving user')
|
throw new Error('error saving user')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Store EmailOptIn in DB
|
const emailOptIn = newEmailOptIn(dbUser.id)
|
||||||
// TODO: this has duplicate code with sendResetPasswordEmail
|
await queryRunner.manager.save(emailOptIn).catch((error) => {
|
||||||
const emailOptIn = await createEmailOptIn(dbUser.id, queryRunner)
|
// 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(
|
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||||
/{optin}/g,
|
/{optin}/g,
|
||||||
@ -378,6 +369,7 @@ export class UserResolver {
|
|||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
email,
|
email,
|
||||||
|
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
|
||||||
})
|
})
|
||||||
|
|
||||||
/* uncomment this, when you need the activation link on the console
|
/* uncomment this, when you need the activation link on the console
|
||||||
@ -398,70 +390,26 @@ export class UserResolver {
|
|||||||
return new User(dbUser)
|
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<boolean> {
|
|
||||||
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])
|
@Authorized([RIGHTS.SEND_RESET_PASSWORD_EMAIL])
|
||||||
@Query(() => Boolean)
|
@Query(() => Boolean)
|
||||||
async sendResetPasswordEmail(@Arg('email') email: string): Promise<boolean> {
|
async sendResetPasswordEmail(@Arg('email') email: string): Promise<boolean> {
|
||||||
// TODO: this has duplicate code with createUser
|
|
||||||
email = email.trim().toLowerCase()
|
email = email.trim().toLowerCase()
|
||||||
const user = await DbUser.findOneOrFail({ email })
|
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(
|
optInCode = await checkOptInCode(optInCode, user.id, OptInType.EMAIL_OPT_IN_RESET_PASSWORD)
|
||||||
/{optin}/g,
|
|
||||||
optInCode.verificationCode.toString(),
|
|
||||||
)
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const emailSent = await sendResetPasswordEmail({
|
const emailSent = await sendResetPasswordEmailMailer({
|
||||||
link,
|
link: activationLink(optInCode),
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
email,
|
email,
|
||||||
|
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
|
||||||
})
|
})
|
||||||
|
|
||||||
/* uncomment this, when you need the activation link on the console
|
/* 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
|
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
||||||
if (!isOptInCodeValid(optInCode)) {
|
if (!isOptInValid(optInCode)) {
|
||||||
throw new Error(`email already more than $(CONFIG.EMAIL_CODE_VALID_TIME} minutes ago`)
|
throw new Error(
|
||||||
|
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// load user
|
// load user
|
||||||
@ -538,11 +488,6 @@ export class UserResolver {
|
|||||||
throw new Error('error saving user: ' + error)
|
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()
|
await queryRunner.commitTransaction()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
@ -553,7 +498,7 @@ export class UserResolver {
|
|||||||
|
|
||||||
// Sign into Klicktipp
|
// Sign into Klicktipp
|
||||||
// TODO do we always signUp the user? How to handle things with old users?
|
// 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 {
|
try {
|
||||||
await klicktippSignIn(user.email, user.language, user.firstName, user.lastName)
|
await klicktippSignIn(user.email, user.language, user.firstName, user.lastName)
|
||||||
} catch {
|
} catch {
|
||||||
@ -573,8 +518,10 @@ export class UserResolver {
|
|||||||
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
|
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
|
||||||
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: optIn })
|
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: optIn })
|
||||||
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
||||||
if (!isOptInCodeValid(optInCode)) {
|
if (!isOptInValid(optInCode)) {
|
||||||
throw new Error(`email was sent more than $(CONFIG.EMAIL_CODE_VALID_TIME} minutes ago`)
|
throw new Error(
|
||||||
|
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -680,7 +627,34 @@ export class UserResolver {
|
|||||||
return hasElopageBuys(userEntity.email)
|
return hasElopageBuys(userEntity.email)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function isOptInCodeValid(optInCode: LoginEmailOptIn) {
|
|
||||||
const timeElapsed = Date.now() - new Date(optInCode.updatedAt).getTime()
|
const isTimeExpired = (optIn: LoginEmailOptIn, duration: number): boolean => {
|
||||||
return timeElapsed <= CONFIG.EMAIL_CODE_VALID_TIME * 60 * 1000
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ describe('sendAccountActivationEmail', () => {
|
|||||||
firstName: 'Peter',
|
firstName: 'Peter',
|
||||||
lastName: 'Lustig',
|
lastName: 'Lustig',
|
||||||
email: 'peter@lustig.de',
|
email: 'peter@lustig.de',
|
||||||
|
duration: '23 hours and 30 minutes',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -23,7 +24,9 @@ describe('sendAccountActivationEmail', () => {
|
|||||||
to: `Peter Lustig <peter@lustig.de>`,
|
to: `Peter Lustig <peter@lustig.de>`,
|
||||||
subject: 'Gradido: E-Mail Überprüfung',
|
subject: 'Gradido: E-Mail Überprüfung',
|
||||||
text:
|
text:
|
||||||
expect.stringContaining('Hallo Peter Lustig') && expect.stringContaining('activationLink'),
|
expect.stringContaining('Hallo Peter Lustig') &&
|
||||||
|
expect.stringContaining('activationLink') &&
|
||||||
|
expect.stringContaining('23 Stunden und 30 Minuten'),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
import { sendEMail } from './sendEMail'
|
import { sendEMail } from './sendEMail'
|
||||||
import { accountActivation } from './text/accountActivation'
|
import { accountActivation } from './text/accountActivation'
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
|
||||||
export const sendAccountActivationEmail = (data: {
|
export const sendAccountActivationEmail = (data: {
|
||||||
link: string
|
link: string
|
||||||
firstName: string
|
firstName: string
|
||||||
lastName: string
|
lastName: string
|
||||||
email: string
|
email: string
|
||||||
|
duration: string
|
||||||
}): Promise<boolean> => {
|
}): Promise<boolean> => {
|
||||||
return sendEMail({
|
return sendEMail({
|
||||||
to: `${data.firstName} ${data.lastName} <${data.email}>`,
|
to: `${data.firstName} ${data.lastName} <${data.email}>`,
|
||||||
subject: accountActivation.de.subject,
|
subject: accountActivation.de.subject,
|
||||||
text: accountActivation.de.text(data),
|
text: accountActivation.de.text({ ...data, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ describe('sendResetPasswordEmail', () => {
|
|||||||
firstName: 'Peter',
|
firstName: 'Peter',
|
||||||
lastName: 'Lustig',
|
lastName: 'Lustig',
|
||||||
email: 'peter@lustig.de',
|
email: 'peter@lustig.de',
|
||||||
|
duration: '23 hours and 30 minutes',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -22,7 +23,10 @@ describe('sendResetPasswordEmail', () => {
|
|||||||
expect(sendEMail).toBeCalledWith({
|
expect(sendEMail).toBeCalledWith({
|
||||||
to: `Peter Lustig <peter@lustig.de>`,
|
to: `Peter Lustig <peter@lustig.de>`,
|
||||||
subject: 'Gradido: Passwort zurücksetzen',
|
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'),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
import { sendEMail } from './sendEMail'
|
import { sendEMail } from './sendEMail'
|
||||||
import { resetPassword } from './text/resetPassword'
|
import { resetPassword } from './text/resetPassword'
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
|
||||||
export const sendResetPasswordEmail = (data: {
|
export const sendResetPasswordEmail = (data: {
|
||||||
link: string
|
link: string
|
||||||
firstName: string
|
firstName: string
|
||||||
lastName: string
|
lastName: string
|
||||||
email: string
|
email: string
|
||||||
|
duration: string
|
||||||
}): Promise<boolean> => {
|
}): Promise<boolean> => {
|
||||||
return sendEMail({
|
return sendEMail({
|
||||||
to: `${data.firstName} ${data.lastName} <${data.email}>`,
|
to: `${data.firstName} ${data.lastName} <${data.email}>`,
|
||||||
subject: resetPassword.de.subject,
|
subject: resetPassword.de.subject,
|
||||||
text: resetPassword.de.text(data),
|
text: resetPassword.de.text({ ...data, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
export const accountActivation = {
|
export const accountActivation = {
|
||||||
de: {
|
de: {
|
||||||
subject: 'Gradido: E-Mail Überprüfung',
|
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},
|
`Hallo ${data.firstName} ${data.lastName},
|
||||||
|
|
||||||
Deine E-Mail-Adresse wurde soeben bei Gradido registriert.
|
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}
|
${data.link}
|
||||||
oder kopiere den obigen Link in dein Browserfenster.
|
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,
|
Mit freundlichen Grüßen,
|
||||||
dein Gradido-Team`,
|
dein Gradido-Team`,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,13 +1,29 @@
|
|||||||
export const resetPassword = {
|
export const resetPassword = {
|
||||||
de: {
|
de: {
|
||||||
subject: 'Gradido: Passwort zurücksetzen',
|
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},
|
`Hallo ${data.firstName} ${data.lastName},
|
||||||
|
|
||||||
Du oder jemand anderes hat für dieses Konto ein Zurücksetzen des Passworts angefordert.
|
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}
|
Wenn du es warst, klicke bitte auf den Link: ${data.link}
|
||||||
oder kopiere den obigen Link in Dein Browserfenster.
|
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,
|
Mit freundlichen Grüßen,
|
||||||
dein Gradido-Team`,
|
dein Gradido-Team`,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -18,7 +18,7 @@ WEBHOOK_GITHUB_SECRET=secret
|
|||||||
WEBHOOK_GITHUB_BRANCH=master
|
WEBHOOK_GITHUB_BRANCH=master
|
||||||
|
|
||||||
# backend
|
# backend
|
||||||
BACKEND_CONFIG_VERSION=v1.2022-03-18
|
BACKEND_CONFIG_VERSION=v3.2022-03-29
|
||||||
|
|
||||||
EMAIL=true
|
EMAIL=true
|
||||||
EMAIL_USERNAME=peter@lustig.de
|
EMAIL_USERNAME=peter@lustig.de
|
||||||
|
|||||||
@ -45,11 +45,16 @@ export const logout = gql`
|
|||||||
export const transactionsQuery = gql`
|
export const transactionsQuery = gql`
|
||||||
query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
|
query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
|
||||||
transactionList(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
|
transactionList(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
|
||||||
balanceGDT
|
balance {
|
||||||
count
|
balance
|
||||||
linkCount
|
decay
|
||||||
balance
|
lastBookedBalance
|
||||||
decayStartBlock
|
balanceGDT
|
||||||
|
count
|
||||||
|
linkCount
|
||||||
|
decayStartBlock
|
||||||
|
lastBookedDate
|
||||||
|
}
|
||||||
transactions {
|
transactions {
|
||||||
id
|
id
|
||||||
typeId
|
typeId
|
||||||
|
|||||||
@ -145,11 +145,13 @@ describe('DashboardLayoutGdd', () => {
|
|||||||
apolloMock.mockResolvedValue({
|
apolloMock.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
transactionList: {
|
transactionList: {
|
||||||
balanceGDT: 100,
|
balance: {
|
||||||
count: 4,
|
balanceGDT: 100,
|
||||||
linkCount: 8,
|
count: 4,
|
||||||
balance: 1450,
|
linkCount: 8,
|
||||||
decay: 1250,
|
balance: 1450,
|
||||||
|
decay: 1250,
|
||||||
|
},
|
||||||
transactions: ['transaction', 'transaction', 'transaction', 'transaction'],
|
transactions: ['transaction', 'transaction', 'transaction', 'transaction'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -103,12 +103,14 @@ export default {
|
|||||||
data: { transactionList },
|
data: { transactionList },
|
||||||
} = result
|
} = result
|
||||||
this.GdtBalance =
|
this.GdtBalance =
|
||||||
transactionList.balanceGDT === null ? null : Number(transactionList.balanceGDT)
|
transactionList.balance.balanceGDT === null
|
||||||
|
? null
|
||||||
|
: Number(transactionList.balance.balanceGDT)
|
||||||
this.transactions = transactionList.transactions
|
this.transactions = transactionList.transactions
|
||||||
this.balance = Number(transactionList.balance)
|
this.balance = Number(transactionList.balance.balance)
|
||||||
this.transactionCount = transactionList.count
|
this.transactionCount = transactionList.balance.count
|
||||||
this.transactionLinkCount = transactionList.linkCount
|
this.transactionLinkCount = transactionList.balance.linkCount
|
||||||
this.decayStartBlock = new Date(transactionList.decayStartBlock)
|
this.decayStartBlock = new Date(transactionList.balance.decayStartBlock)
|
||||||
this.pending = false
|
this.pending = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@ -150,13 +150,18 @@ describe('ResetPassword', () => {
|
|||||||
|
|
||||||
describe('server response with error code > 10min', () => {
|
describe('server response with error code > 10min', () => {
|
||||||
beforeEach(async () => {
|
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 wrapper.find('form').trigger('submit')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('toasts an error message', () => {
|
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', () => {
|
it('router pushes to /forgot-password/resetPassword', () => {
|
||||||
|
|||||||
@ -108,7 +108,11 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.toastError(error.message)
|
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')
|
this.$router.push('/forgot-password/resetPassword')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user