mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into archive_transform_valid_transactions
This commit is contained in:
commit
6423c66b0b
15
CHANGELOG.md
15
CHANGELOG.md
@ -4,8 +4,23 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [1.10.1](https://github.com/gradido/gradido/compare/1.10.0...1.10.1)
|
||||
|
||||
- automatic session logout with info modal [`#2001`](https://github.com/gradido/gradido/pull/2001)
|
||||
- 1910 separate text for the slideshow images. [`#1998`](https://github.com/gradido/gradido/pull/1998)
|
||||
- Origin/1921 additional parameter checks for createContributionLinks [`#1996`](https://github.com/gradido/gradido/pull/1996)
|
||||
- add missing locales [`#1999`](https://github.com/gradido/gradido/pull/1999)
|
||||
- 1906 feature concept for gdd creation per linkqr code [`#1907`](https://github.com/gradido/gradido/pull/1907)
|
||||
- refactor: 🍰 Not Throwing An Error When Register With Existing Email [`#1962`](https://github.com/gradido/gradido/pull/1962)
|
||||
- feat: 🍰 Set Role In Admin Interface [`#1974`](https://github.com/gradido/gradido/pull/1974)
|
||||
- refactor mobile style step 1 [`#1977`](https://github.com/gradido/gradido/pull/1977)
|
||||
- changed mobil stage picture [`#1995`](https://github.com/gradido/gradido/pull/1995)
|
||||
|
||||
#### [1.10.0](https://github.com/gradido/gradido/compare/1.9.0...1.10.0)
|
||||
|
||||
> 17 June 2022
|
||||
|
||||
- release: v1.10.0 [`#1993`](https://github.com/gradido/gradido/pull/1993)
|
||||
- frontend redeem contribution link [`#1988`](https://github.com/gradido/gradido/pull/1988)
|
||||
- change new start picture [`#1990`](https://github.com/gradido/gradido/pull/1990)
|
||||
- feat: Redeem Contribution Link [`#1987`](https://github.com/gradido/gradido/pull/1987)
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"description": "Administraion Interface for Gradido",
|
||||
"main": "index.js",
|
||||
"author": "Moriz Wahl",
|
||||
"version": "1.10.0",
|
||||
"version": "1.10.1",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
|
||||
@ -3,7 +3,7 @@ CONFIG_VERSION=v8.2022-06-20
|
||||
# Server
|
||||
PORT=4000
|
||||
JWT_SECRET=secret123
|
||||
JWT_EXPIRES_IN=30m
|
||||
JWT_EXPIRES_IN=10m
|
||||
GRAPHIQL=false
|
||||
GDT_API_URL=https://gdt.gradido.net
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-backend",
|
||||
"version": "1.10.0",
|
||||
"version": "1.10.1",
|
||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/backend",
|
||||
|
||||
@ -25,6 +25,7 @@ export enum RIGHTS {
|
||||
REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK',
|
||||
LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS',
|
||||
GDT_BALANCE = 'GDT_BALANCE',
|
||||
CREATE_CONTRIBUTION = 'CREATE_CONTRIBUTION',
|
||||
// Admin
|
||||
SEARCH_USERS = 'SEARCH_USERS',
|
||||
SET_USER_ROLE = 'SET_USER_ROLE',
|
||||
|
||||
@ -23,6 +23,7 @@ export const ROLE_USER = new Role('user', [
|
||||
RIGHTS.REDEEM_TRANSACTION_LINK,
|
||||
RIGHTS.LIST_TRANSACTION_LINKS,
|
||||
RIGHTS.GDT_BALANCE,
|
||||
RIGHTS.CREATE_CONTRIBUTION,
|
||||
])
|
||||
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ const constants = {
|
||||
const server = {
|
||||
PORT: process.env.PORT || 4000,
|
||||
JWT_SECRET: process.env.JWT_SECRET || 'secret123',
|
||||
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '30m',
|
||||
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m',
|
||||
GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
|
||||
GDT_API_URL: process.env.GDT_API_URL || 'https://gdt.gradido.net',
|
||||
PRODUCTION: process.env.NODE_ENV === 'production' || false,
|
||||
|
||||
15
backend/src/graphql/arg/ContributionArgs.ts
Normal file
15
backend/src/graphql/arg/ContributionArgs.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ArgsType, Field, InputType } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@InputType()
|
||||
@ArgsType()
|
||||
export default class ContributionArgs {
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
|
||||
@Field(() => String)
|
||||
creationDate: string
|
||||
}
|
||||
@ -1,8 +1,22 @@
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import { User } from '@entity/User'
|
||||
|
||||
@ObjectType()
|
||||
export class UnconfirmedContribution {
|
||||
constructor(contribution: Contribution, user: User, creations: Decimal[]) {
|
||||
this.id = contribution.id
|
||||
this.userId = contribution.userId
|
||||
this.amount = contribution.amount
|
||||
this.memo = contribution.memo
|
||||
this.date = contribution.contributionDate
|
||||
this.firstName = user ? user.firstName : ''
|
||||
this.lastName = user ? user.lastName : ''
|
||||
this.email = user ? user.email : ''
|
||||
this.creation = creations
|
||||
}
|
||||
|
||||
@Field(() => String)
|
||||
firstName: string
|
||||
|
||||
@ -27,8 +41,8 @@ export class UnconfirmedContribution {
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => Number)
|
||||
moderator: number
|
||||
@Field(() => Number, { nullable: true })
|
||||
moderator: number | null
|
||||
|
||||
@Field(() => [Decimal])
|
||||
creation: Decimal[]
|
||||
|
||||
@ -46,15 +46,23 @@ import { checkOptInCode, activationLink, printTimeDuration } from './UserResolve
|
||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
||||
import CONFIG from '@/config'
|
||||
import {
|
||||
getCreationIndex,
|
||||
getUserCreation,
|
||||
getUserCreations,
|
||||
validateContribution,
|
||||
isStartEndDateValid,
|
||||
} from './util/creations'
|
||||
import {
|
||||
CONTRIBUTIONLINK_MEMO_MAX_CHARS,
|
||||
CONTRIBUTIONLINK_MEMO_MIN_CHARS,
|
||||
CONTRIBUTIONLINK_NAME_MAX_CHARS,
|
||||
CONTRIBUTIONLINK_NAME_MIN_CHARS,
|
||||
FULL_CREATION_AVAILABLE,
|
||||
} from './const/const'
|
||||
|
||||
// const EMAIL_OPT_IN_REGISTER = 1
|
||||
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
||||
const MAX_CREATION_AMOUNT = new Decimal(1000)
|
||||
const FULL_CREATION_AVAILABLE = [MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT]
|
||||
const CONTRIBUTIONLINK_NAME_MAX_CHARS = 100
|
||||
const CONTRIBUTIONLINK_NAME_MIN_CHARS = 5
|
||||
const CONTRIBUTIONLINK_MEMO_MAX_CHARS = 255
|
||||
const CONTRIBUTIONLINK_MEMO_MIN_CHARS = 5
|
||||
|
||||
@Resolver()
|
||||
export class AdminResolver {
|
||||
@ -244,18 +252,17 @@ export class AdminResolver {
|
||||
const creations = await getUserCreation(user.id)
|
||||
logger.trace('creations', creations)
|
||||
const creationDateObj = new Date(creationDate)
|
||||
if (isContributionValid(creations, amount, creationDateObj)) {
|
||||
const contribution = Contribution.create()
|
||||
contribution.userId = user.id
|
||||
contribution.amount = amount
|
||||
contribution.createdAt = new Date()
|
||||
contribution.contributionDate = creationDateObj
|
||||
contribution.memo = memo
|
||||
contribution.moderatorId = moderator.id
|
||||
validateContribution(creations, amount, creationDateObj)
|
||||
const contribution = Contribution.create()
|
||||
contribution.userId = user.id
|
||||
contribution.amount = amount
|
||||
contribution.createdAt = new Date()
|
||||
contribution.contributionDate = creationDateObj
|
||||
contribution.memo = memo
|
||||
contribution.moderatorId = moderator.id
|
||||
|
||||
logger.trace('contribution to save', contribution)
|
||||
await Contribution.save(contribution)
|
||||
}
|
||||
logger.trace('contribution to save', contribution)
|
||||
await Contribution.save(contribution)
|
||||
return getUserCreation(user.id)
|
||||
}
|
||||
|
||||
@ -321,7 +328,7 @@ export class AdminResolver {
|
||||
}
|
||||
|
||||
// all possible cases not to be true are thrown in this function
|
||||
isContributionValid(creations, amount, creationDateObj)
|
||||
validateContribution(creations, amount, creationDateObj)
|
||||
contributionToUpdate.amount = amount
|
||||
contributionToUpdate.memo = memo
|
||||
contributionToUpdate.contributionDate = new Date(creationDate)
|
||||
@ -398,9 +405,7 @@ export class AdminResolver {
|
||||
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a contribution.')
|
||||
|
||||
const creations = await getUserCreation(contribution.userId, false)
|
||||
if (!isContributionValid(creations, contribution.amount, contribution.contributionDate)) {
|
||||
throw new Error('Creation is not valid!!')
|
||||
}
|
||||
validateContribution(creations, contribution.amount, contribution.contributionDate)
|
||||
|
||||
const receivedCallDate = new Date()
|
||||
|
||||
@ -684,64 +689,6 @@ export class AdminResolver {
|
||||
}
|
||||
}
|
||||
|
||||
interface CreationMap {
|
||||
id: number
|
||||
creations: Decimal[]
|
||||
}
|
||||
|
||||
export const getUserCreation = async (id: number, includePending = true): Promise<Decimal[]> => {
|
||||
logger.trace('getUserCreation', id, includePending)
|
||||
const creations = await getUserCreations([id], includePending)
|
||||
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
||||
}
|
||||
|
||||
async function getUserCreations(ids: number[], includePending = true): Promise<CreationMap[]> {
|
||||
logger.trace('getUserCreations:', ids, includePending)
|
||||
const months = getCreationMonths()
|
||||
logger.trace('getUserCreations months', months)
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
|
||||
const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day'
|
||||
logger.trace('getUserCreations dateFilter', dateFilter)
|
||||
|
||||
const unionString = includePending
|
||||
? `
|
||||
UNION
|
||||
SELECT contribution_date AS date, amount AS amount, user_id AS userId FROM contributions
|
||||
WHERE user_id IN (${ids.toString()})
|
||||
AND contribution_date >= ${dateFilter}
|
||||
AND confirmed_at IS NULL AND deleted_at IS NULL`
|
||||
: ''
|
||||
|
||||
const unionQuery = await queryRunner.manager.query(`
|
||||
SELECT MONTH(date) AS month, sum(amount) AS sum, userId AS id FROM
|
||||
(SELECT creation_date AS date, amount AS amount, user_id AS userId FROM transactions
|
||||
WHERE user_id IN (${ids.toString()})
|
||||
AND type_id = ${TransactionTypeId.CREATION}
|
||||
AND creation_date >= ${dateFilter}
|
||||
${unionString}) AS result
|
||||
GROUP BY month, userId
|
||||
ORDER BY date DESC
|
||||
`)
|
||||
|
||||
await queryRunner.release()
|
||||
|
||||
return ids.map((id) => {
|
||||
return {
|
||||
id,
|
||||
creations: months.map((month) => {
|
||||
const creation = unionQuery.find(
|
||||
(raw: { month: string; id: string; creation: number[] }) =>
|
||||
parseInt(raw.month) === month && parseInt(raw.id) === id,
|
||||
)
|
||||
return MAX_CREATION_AMOUNT.minus(creation ? creation.sum : 0)
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function updateCreations(creations: Decimal[], contribution: Contribution): Decimal[] {
|
||||
const index = getCreationIndex(contribution.contributionDate.getMonth())
|
||||
|
||||
@ -751,58 +698,3 @@ function updateCreations(creations: Decimal[], contribution: Contribution): Deci
|
||||
creations[index] = creations[index].plus(contribution.amount.toString())
|
||||
return creations
|
||||
}
|
||||
|
||||
export const isContributionValid = (
|
||||
creations: Decimal[],
|
||||
amount: Decimal,
|
||||
creationDate: Date,
|
||||
): boolean => {
|
||||
logger.trace('isContributionValid', creations, amount, creationDate)
|
||||
const index = getCreationIndex(creationDate.getMonth())
|
||||
|
||||
if (index < 0) {
|
||||
throw new Error('No information for available creations for the given date')
|
||||
}
|
||||
|
||||
if (amount.greaterThan(creations[index].toString())) {
|
||||
throw new Error(
|
||||
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const isStartEndDateValid = (
|
||||
startDate: string | null | undefined,
|
||||
endDate: string | null | undefined,
|
||||
): void => {
|
||||
if (!startDate) {
|
||||
logger.error('Start-Date is not initialized. A Start-Date must be set!')
|
||||
throw new Error('Start-Date is not initialized. A Start-Date must be set!')
|
||||
}
|
||||
|
||||
if (!endDate) {
|
||||
logger.error('End-Date is not initialized. An End-Date must be set!')
|
||||
throw new Error('End-Date is not initialized. An End-Date must be set!')
|
||||
}
|
||||
|
||||
// check if endDate is before startDate
|
||||
if (new Date(endDate).getTime() - new Date(startDate).getTime() < 0) {
|
||||
logger.error(`The value of validFrom must before or equals the validTo!`)
|
||||
throw new Error(`The value of validFrom must before or equals the validTo!`)
|
||||
}
|
||||
}
|
||||
|
||||
const getCreationMonths = (): number[] => {
|
||||
const now = new Date(Date.now())
|
||||
return [
|
||||
now.getMonth() + 1,
|
||||
new Date(now.getFullYear(), now.getMonth() - 1, 1).getMonth() + 1,
|
||||
new Date(now.getFullYear(), now.getMonth() - 2, 1).getMonth() + 1,
|
||||
].reverse()
|
||||
}
|
||||
|
||||
const getCreationIndex = (month: number): number => {
|
||||
return getCreationMonths().findIndex((el) => el === month + 1)
|
||||
}
|
||||
|
||||
124
backend/src/graphql/resolver/ContributionResolver.test.ts
Normal file
124
backend/src/graphql/resolver/ContributionResolver.test.ts
Normal file
@ -0,0 +1,124 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { createContribution } from '@/seeds/graphql/mutations'
|
||||
import { login } from '@/seeds/graphql/queries'
|
||||
import { cleanDB, resetToken, testEnvironment } from '@test/helpers'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
|
||||
let mutate: any, query: any, con: any
|
||||
let testEnv: any
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment()
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.close()
|
||||
})
|
||||
|
||||
describe('ContributionResolver', () => {
|
||||
describe('createContribution', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('returns an error', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContribution,
|
||||
variables: { amount: 100.0, memo: 'Test Contribution', creationDate: 'not-valid' },
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('401 Unauthorized')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated with valid user', () => {
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
})
|
||||
|
||||
describe('input not valid', () => {
|
||||
it('throws error when creationDate not-valid', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: 'not-valid',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError('No information for available creations for the given date'),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('throws error when creationDate 3 month behind', async () => {
|
||||
const date = new Date()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: date.setMonth(date.getMonth() - 3).toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError('No information for available creations for the given date'),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('valid input', () => {
|
||||
it('creates contribution', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
createContribution: {
|
||||
amount: '100',
|
||||
memo: 'Test env contribution',
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
35
backend/src/graphql/resolver/ContributionResolver.ts
Normal file
35
backend/src/graphql/resolver/ContributionResolver.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { Context, getUser } from '@/server/context'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import { Args, Authorized, Ctx, Mutation, Resolver } from 'type-graphql'
|
||||
import ContributionArgs from '../arg/ContributionArgs'
|
||||
import { UnconfirmedContribution } from '../model/UnconfirmedContribution'
|
||||
import { validateContribution, getUserCreation } from './util/creations'
|
||||
|
||||
@Resolver()
|
||||
export class ContributionResolver {
|
||||
@Authorized([RIGHTS.CREATE_CONTRIBUTION])
|
||||
@Mutation(() => UnconfirmedContribution)
|
||||
async createContribution(
|
||||
@Args() { amount, memo, creationDate }: ContributionArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<UnconfirmedContribution> {
|
||||
const user = getUser(context)
|
||||
const creations = await getUserCreation(user.id)
|
||||
logger.trace('creations', creations)
|
||||
const creationDateObj = new Date(creationDate)
|
||||
validateContribution(creations, amount, creationDateObj)
|
||||
|
||||
const contribution = Contribution.create()
|
||||
contribution.userId = user.id
|
||||
contribution.amount = amount
|
||||
contribution.createdAt = new Date()
|
||||
contribution.contributionDate = creationDateObj
|
||||
contribution.memo = memo
|
||||
|
||||
logger.trace('contribution to save', contribution)
|
||||
await Contribution.save(contribution)
|
||||
return new UnconfirmedContribution(contribution, user, creations)
|
||||
}
|
||||
}
|
||||
@ -28,7 +28,7 @@ import { executeTransaction } from './TransactionResolver'
|
||||
import { Order } from '@enum/Order'
|
||||
import { Contribution as DbContribution } from '@entity/Contribution'
|
||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||
import { getUserCreation, isContributionValid } from './AdminResolver'
|
||||
import { getUserCreation, validateContribution } from './util/creations'
|
||||
import { Decay } from '@model/Decay'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||
@ -223,13 +223,7 @@ export class TransactionLinkResolver {
|
||||
|
||||
const creations = await getUserCreation(user.id, false)
|
||||
logger.info('open creations', creations)
|
||||
if (!isContributionValid(creations, contributionLink.amount, now)) {
|
||||
logger.error(
|
||||
'Amount of Contribution link exceeds available amount for this month',
|
||||
contributionLink.amount,
|
||||
)
|
||||
throw new Error('Amount of Contribution link exceeds available amount')
|
||||
}
|
||||
validateContribution(creations, contributionLink.amount, now)
|
||||
const contribution = new DbContribution()
|
||||
contribution.userId = user.id
|
||||
contribution.createdAt = now
|
||||
|
||||
12
backend/src/graphql/resolver/const/const.ts
Normal file
12
backend/src/graphql/resolver/const/const.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export const MAX_CREATION_AMOUNT = new Decimal(1000)
|
||||
export const FULL_CREATION_AVAILABLE = [
|
||||
MAX_CREATION_AMOUNT,
|
||||
MAX_CREATION_AMOUNT,
|
||||
MAX_CREATION_AMOUNT,
|
||||
]
|
||||
export const CONTRIBUTIONLINK_NAME_MAX_CHARS = 100
|
||||
export const CONTRIBUTIONLINK_NAME_MIN_CHARS = 5
|
||||
export const CONTRIBUTIONLINK_MEMO_MAX_CHARS = 255
|
||||
export const CONTRIBUTIONLINK_MEMO_MIN_CHARS = 5
|
||||
119
backend/src/graphql/resolver/util/creations.ts
Normal file
119
backend/src/graphql/resolver/util/creations.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { getConnection } from '@dbTools/typeorm'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { FULL_CREATION_AVAILABLE, MAX_CREATION_AMOUNT } from '../const/const'
|
||||
|
||||
interface CreationMap {
|
||||
id: number
|
||||
creations: Decimal[]
|
||||
}
|
||||
|
||||
export const validateContribution = (
|
||||
creations: Decimal[],
|
||||
amount: Decimal,
|
||||
creationDate: Date,
|
||||
): void => {
|
||||
logger.trace('isContributionValid', creations, amount, creationDate)
|
||||
const index = getCreationIndex(creationDate.getMonth())
|
||||
|
||||
if (index < 0) {
|
||||
throw new Error('No information for available creations for the given date')
|
||||
}
|
||||
|
||||
if (amount.greaterThan(creations[index].toString())) {
|
||||
throw new Error(
|
||||
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const getUserCreations = async (
|
||||
ids: number[],
|
||||
includePending = true,
|
||||
): Promise<CreationMap[]> => {
|
||||
logger.trace('getUserCreations:', ids, includePending)
|
||||
const months = getCreationMonths()
|
||||
logger.trace('getUserCreations months', months)
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
|
||||
const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day'
|
||||
logger.trace('getUserCreations dateFilter', dateFilter)
|
||||
|
||||
const unionString = includePending
|
||||
? `
|
||||
UNION
|
||||
SELECT contribution_date AS date, amount AS amount, user_id AS userId FROM contributions
|
||||
WHERE user_id IN (${ids.toString()})
|
||||
AND contribution_date >= ${dateFilter}
|
||||
AND confirmed_at IS NULL AND deleted_at IS NULL`
|
||||
: ''
|
||||
|
||||
const unionQuery = await queryRunner.manager.query(`
|
||||
SELECT MONTH(date) AS month, sum(amount) AS sum, userId AS id FROM
|
||||
(SELECT creation_date AS date, amount AS amount, user_id AS userId FROM transactions
|
||||
WHERE user_id IN (${ids.toString()})
|
||||
AND type_id = ${TransactionTypeId.CREATION}
|
||||
AND creation_date >= ${dateFilter}
|
||||
${unionString}) AS result
|
||||
GROUP BY month, userId
|
||||
ORDER BY date DESC
|
||||
`)
|
||||
|
||||
await queryRunner.release()
|
||||
|
||||
return ids.map((id) => {
|
||||
return {
|
||||
id,
|
||||
creations: months.map((month) => {
|
||||
const creation = unionQuery.find(
|
||||
(raw: { month: string; id: string; creation: number[] }) =>
|
||||
parseInt(raw.month) === month && parseInt(raw.id) === id,
|
||||
)
|
||||
return MAX_CREATION_AMOUNT.minus(creation ? creation.sum : 0)
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const getUserCreation = async (id: number, includePending = true): Promise<Decimal[]> => {
|
||||
logger.trace('getUserCreation', id, includePending)
|
||||
const creations = await getUserCreations([id], includePending)
|
||||
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
||||
}
|
||||
|
||||
export const getCreationMonths = (): number[] => {
|
||||
const now = new Date(Date.now())
|
||||
return [
|
||||
now.getMonth() + 1,
|
||||
new Date(now.getFullYear(), now.getMonth() - 1, 1).getMonth() + 1,
|
||||
new Date(now.getFullYear(), now.getMonth() - 2, 1).getMonth() + 1,
|
||||
].reverse()
|
||||
}
|
||||
|
||||
export const getCreationIndex = (month: number): number => {
|
||||
return getCreationMonths().findIndex((el) => el === month + 1)
|
||||
}
|
||||
|
||||
export const isStartEndDateValid = (
|
||||
startDate: string | null | undefined,
|
||||
endDate: string | null | undefined,
|
||||
): void => {
|
||||
if (!startDate) {
|
||||
logger.error('Start-Date is not initialized. A Start-Date must be set!')
|
||||
throw new Error('Start-Date is not initialized. A Start-Date must be set!')
|
||||
}
|
||||
|
||||
if (!endDate) {
|
||||
logger.error('End-Date is not initialized. An End-Date must be set!')
|
||||
throw new Error('End-Date is not initialized. An End-Date must be set!')
|
||||
}
|
||||
|
||||
// check if endDate is before startDate
|
||||
if (new Date(endDate).getTime() - new Date(startDate).getTime() < 0) {
|
||||
logger.error(`The value of validFrom must before or equals the validTo!`)
|
||||
throw new Error(`The value of validFrom must before or equals the validTo!`)
|
||||
}
|
||||
}
|
||||
@ -230,3 +230,12 @@ export const deleteContributionLink = gql`
|
||||
deleteContributionLink(id: $id)
|
||||
}
|
||||
`
|
||||
|
||||
export const createContribution = gql`
|
||||
mutation ($amount: Decimal!, $memo: String!, $creationDate: String!) {
|
||||
createContribution(amount: $amount, memo: $memo, creationDate: $creationDate) {
|
||||
amount
|
||||
memo
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-database",
|
||||
"version": "1.10.0",
|
||||
"version": "1.10.1",
|
||||
"description": "Gradido Database Tool to execute database migrations",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/database",
|
||||
|
||||
@ -28,7 +28,7 @@ COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
|
||||
# backend
|
||||
BACKEND_CONFIG_VERSION=v8.2022-06-20
|
||||
|
||||
JWT_EXPIRES_IN=30m
|
||||
JWT_EXPIRES_IN=10m
|
||||
GDT_API_URL=https://gdt.gradido.net
|
||||
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=../deployment/bare_metal/log/typeorm.backend.log
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bootstrap-vue-gradido-wallet",
|
||||
"version": "1.10.0",
|
||||
"version": "1.10.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node run/server.js",
|
||||
@ -45,6 +45,7 @@
|
||||
"jest": "^26.6.3",
|
||||
"jest-canvas-mock": "^2.3.1",
|
||||
"jest-environment-jsdom-sixteen": "^2.0.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"portal-vue": "^2.1.7",
|
||||
"prettier": "^2.2.1",
|
||||
"qrcanvas-vue": "2.1.1",
|
||||
@ -59,6 +60,7 @@
|
||||
"vue-loading-overlay": "^3.4.2",
|
||||
"vue-moment": "^4.1.0",
|
||||
"vue-router": "^3.0.6",
|
||||
"vue-timers": "^2.0.4",
|
||||
"vue2-transitions": "^0.2.3",
|
||||
"vuex": "^3.6.0",
|
||||
"vuex-persistedstate": "^4.0.0-beta.3"
|
||||
|
||||
@ -46,7 +46,7 @@ export default {
|
||||
|
||||
<style lang="scss">
|
||||
.authNavbar > .nav-link {
|
||||
color: #383838 !important;
|
||||
color: #0e79bc !important;
|
||||
}
|
||||
|
||||
.navbar-toggler {
|
||||
@ -54,7 +54,7 @@ export default {
|
||||
}
|
||||
|
||||
.authNavbar > .router-link-exact-active {
|
||||
color: #0e79bc !important;
|
||||
color: #383838 !important;
|
||||
}
|
||||
|
||||
button.navbar-toggler > span.navbar-toggler-icon {
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
@click.prevent="saveLocale(lang.code)"
|
||||
:key="lang.code"
|
||||
class="pointer pr-2"
|
||||
:class="$store.state.language === lang.code ? 'c-blau' : 'c-grey'"
|
||||
:class="$store.state.language === lang.code ? 'c-grey' : 'c-blau'"
|
||||
>
|
||||
<span class="locales">{{ lang.name }}</span>
|
||||
<span class="ml-3">{{ locales.length - 1 > index ? $t('math.pipe') : '' }}</span>
|
||||
|
||||
99
frontend/src/components/SessionLogoutTimeout.spec.js
Normal file
99
frontend/src/components/SessionLogoutTimeout.spec.js
Normal file
@ -0,0 +1,99 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import SessionLogoutTimeout from './SessionLogoutTimeout'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloQueryMock = jest.fn()
|
||||
|
||||
const setTokenTime = (seconds) => {
|
||||
const now = new Date()
|
||||
return Math.floor(new Date(now.setSeconds(now.getSeconds() + seconds)).getTime() / 1000)
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
$store: {
|
||||
state: {
|
||||
token: '1234',
|
||||
tokenTime: setTokenTime(120),
|
||||
},
|
||||
},
|
||||
$i18n: {
|
||||
locale: 'en',
|
||||
},
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
},
|
||||
$route: {
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
describe('SessionLogoutTimeout', () => {
|
||||
let wrapper, spy
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(SessionLogoutTimeout, { localVue, mocks })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders the component div.session-logout-timeout', () => {
|
||||
expect(wrapper.find('div.session-logout-timeout').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('timers', () => {
|
||||
it('has a token expires timer', () => {
|
||||
expect(wrapper.vm.$options.timers).toEqual({
|
||||
tokenExpires: expect.objectContaining({
|
||||
name: 'tokenExpires',
|
||||
time: 15000,
|
||||
repeat: true,
|
||||
immediate: true,
|
||||
autostart: true,
|
||||
isSwitchTab: false,
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
describe('token is expired', () => {
|
||||
beforeEach(() => {
|
||||
mocks.$store.state.tokenTime = setTokenTime(-60)
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('emits logout', () => {
|
||||
expect(wrapper.emitted('logout')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('token time less than 75 seconds', () => {
|
||||
beforeEach(() => {
|
||||
mocks.$store.state.tokenTime = setTokenTime(60)
|
||||
jest.useFakeTimers()
|
||||
wrapper = Wrapper()
|
||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||
spy.mockImplementation(() => Promise.resolve(true))
|
||||
})
|
||||
|
||||
it('sets the timer to 1000', () => {
|
||||
expect(wrapper.vm.timers.tokenExpires.time).toBe(1000)
|
||||
})
|
||||
|
||||
it.skip('opens the modal', () => {
|
||||
jest.advanceTimersByTime(1000)
|
||||
jest.advanceTimersByTime(1000)
|
||||
jest.advanceTimersByTime(1000)
|
||||
jest.advanceTimersByTime(1000)
|
||||
expect(spy).toBeCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
100
frontend/src/components/SessionLogoutTimeout.vue
Normal file
100
frontend/src/components/SessionLogoutTimeout.vue
Normal file
@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="session-logout-timeout">
|
||||
<b-modal
|
||||
id="modalSessionTimeOut"
|
||||
class="bg-variant-danger"
|
||||
hide-header-close
|
||||
hide-header
|
||||
hide-footer
|
||||
no-close-on-backdrop
|
||||
>
|
||||
<b-card header-tag="header" footer-tag="footer">
|
||||
<b-card-text>
|
||||
<div class="p-3 h2">{{ $t('session.warningText') }}</div>
|
||||
<div class="p-3">
|
||||
{{ $t('session.lightText') }}
|
||||
</div>
|
||||
<div class="p-3 h2 text-warning">
|
||||
{{ $t('session.logoutIn') }}
|
||||
<b>{{ tokenExpiresInSeconds }}</b>
|
||||
{{ $t('time.seconds') }}
|
||||
</div>
|
||||
</b-card-text>
|
||||
<b-row>
|
||||
<b-col class="text-center">
|
||||
<b-button size="lg" variant="success" @click="handleOk">
|
||||
{{ $t('session.extend') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-card>
|
||||
<template #modal-footer>
|
||||
<b-button size="sm" variant="danger" @click="$emit('logout')">
|
||||
{{ $t('navigation.logout') }}
|
||||
</b-button>
|
||||
<b-button size="lg" variant="success" @click="handleOk">
|
||||
{{ $t('session.extend') }}
|
||||
</b-button>
|
||||
</template>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { verifyLogin } from '@/graphql/queries'
|
||||
|
||||
export default {
|
||||
name: 'SessionLogoutTimeout',
|
||||
data() {
|
||||
return {
|
||||
now: new Date().getTime(),
|
||||
}
|
||||
},
|
||||
timers: {
|
||||
tokenExpires: {
|
||||
time: 15000,
|
||||
autostart: true,
|
||||
repeat: true,
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
tokenExpires() {
|
||||
this.now = new Date().getTime()
|
||||
if (this.tokenExpiresInSeconds < 75 && this.timers.tokenExpires.time !== 1000) {
|
||||
this.timers.tokenExpires.time = 1000
|
||||
this.$timer.restart('tokenExpires')
|
||||
this.$bvModal.show('modalSessionTimeOut')
|
||||
}
|
||||
if (this.tokenExpiresInSeconds <= 0) {
|
||||
this.$timer.stop('tokenExpires')
|
||||
this.$emit('logout')
|
||||
}
|
||||
},
|
||||
handleOk(bvModalEvent) {
|
||||
bvModalEvent.preventDefault()
|
||||
this.$apollo
|
||||
.query({
|
||||
query: verifyLogin,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
.then((result) => {
|
||||
this.timers.tokenExpires.time = 15000
|
||||
this.$timer.restart('tokenExpires')
|
||||
this.$bvModal.hide('modalSessionTimeOut')
|
||||
})
|
||||
.catch(() => {
|
||||
this.$timer.stop('tokenExpires')
|
||||
this.$emit('logout')
|
||||
})
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
tokenExpiresInSeconds() {
|
||||
return Math.floor((new Date(this.$store.state.tokenTime * 1000).getTime() - this.now) / 1000)
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$timer.stop('tokenExpires')
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -9,15 +9,16 @@ const mockAPIcall = jest.fn()
|
||||
const navigatorClipboardMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$i18n: {
|
||||
locale: 'en',
|
||||
},
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => d),
|
||||
$tc: jest.fn((tc) => tc),
|
||||
$apollo: {
|
||||
mutate: mockAPIcall,
|
||||
},
|
||||
$store: {
|
||||
state: {
|
||||
firstName: 'Testy',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
@ -77,7 +78,7 @@ describe('TransactionLink', () => {
|
||||
navigator.clipboard = navigatorClipboard
|
||||
})
|
||||
|
||||
describe('copy with success', () => {
|
||||
describe('copy link with success', () => {
|
||||
beforeEach(async () => {
|
||||
navigatorClipboardMock.mockResolvedValue()
|
||||
await wrapper.find('.test-copy-link .dropdown-item').trigger('click')
|
||||
@ -92,6 +93,47 @@ describe('TransactionLink', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.link-copied')
|
||||
})
|
||||
})
|
||||
|
||||
describe('copy link and text with success', () => {
|
||||
beforeEach(async () => {
|
||||
navigatorClipboardMock.mockResolvedValue()
|
||||
await wrapper.find('.test-copy-text .dropdown-item').trigger('click')
|
||||
})
|
||||
|
||||
it('should call clipboard.writeText', () => {
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
|
||||
'http://localhost/redeem/c00000000c000000c0000\n' +
|
||||
'Testy transaction-link.send_you 75 Gradido.\n' +
|
||||
'"Katzenauge, Eulenschrei, was verschwunden komm herbei!"\n' +
|
||||
'gdd_per_link.credit-your-gradido gdd_per_link.validUntilDate',
|
||||
)
|
||||
})
|
||||
it('toasts success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.link-and-text-copied')
|
||||
})
|
||||
})
|
||||
|
||||
describe('copy link with error', () => {
|
||||
beforeEach(async () => {
|
||||
navigatorClipboardMock.mockRejectedValue()
|
||||
await wrapper.find('.test-copy-link .dropdown-item').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('gdd_per_link.not-copied')
|
||||
})
|
||||
})
|
||||
|
||||
describe('copy link and text with error', () => {
|
||||
beforeEach(async () => {
|
||||
navigatorClipboardMock.mockRejectedValue()
|
||||
await wrapper.find('.test-copy-text .dropdown-item').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('gdd_per_link.not-copied')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('qr code modal', () => {
|
||||
|
||||
@ -22,6 +22,14 @@
|
||||
<b-icon icon="clipboard"></b-icon>
|
||||
{{ $t('gdd_per_link.copy') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item
|
||||
v-if="validLink"
|
||||
class="test-copy-text pt-3"
|
||||
@click="copyLinkWithText()"
|
||||
>
|
||||
<b-icon icon="clipboard-plus"></b-icon>
|
||||
{{ $t('gdd_per_link.copy-with-text') }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item
|
||||
v-if="validLink"
|
||||
@click="$bvModal.show('modalPopover-' + id)"
|
||||
@ -99,6 +107,24 @@ export default {
|
||||
this.toastError(this.$t('gdd_per_link.not-copied'))
|
||||
})
|
||||
},
|
||||
copyLinkWithText() {
|
||||
navigator.clipboard
|
||||
.writeText(
|
||||
`${this.link}
|
||||
${this.$store.state.firstName} ${this.$t('transaction-link.send_you')} ${this.amount} Gradido.
|
||||
"${this.memo}"
|
||||
${this.$t('gdd_per_link.credit-your-gradido')} ${this.$t('gdd_per_link.validUntilDate', {
|
||||
date: this.$d(new Date(this.validUntil), 'short'),
|
||||
})}`,
|
||||
)
|
||||
.then(() => {
|
||||
this.toastSuccess(this.$t('gdd_per_link.link-and-text-copied'))
|
||||
})
|
||||
.catch(() => {
|
||||
this.$bvModal.show('modalPopoverCopyError' + this.id)
|
||||
this.toastError(this.$t('gdd_per_link.not-copied'))
|
||||
})
|
||||
},
|
||||
deleteLink() {
|
||||
this.$bvModal.msgBoxConfirm(this.$t('gdd_per_link.delete-the-link')).then(async (value) => {
|
||||
if (value)
|
||||
|
||||
@ -35,9 +35,10 @@ describe('AuthLayout', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
describe('Mobile Version Start', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.vm.mobileStart = true
|
||||
wrapper.findComponent({ name: 'AuthMobileStart' }).vm.$emit('set-mobile-start', true)
|
||||
})
|
||||
|
||||
it('has Component AuthMobileStart', () => {
|
||||
|
||||
@ -30,8 +30,8 @@
|
||||
</b-row>
|
||||
<b-row class="mt-0 mt-md-5 pl-2 pl-md-0 pl-lg-0">
|
||||
<b-col lg="9" md="9" sm="12">
|
||||
<div class="h1 mb--2">{{ $t('welcome') }}</div>
|
||||
<div class="h1 mb-0">{{ $t('WelcomeBy', { name: communityName }) }}</div>
|
||||
<div class="mb--2">{{ $t('welcome') }}</div>
|
||||
<div class="h1 mb-0">{{ communityName }}</div>
|
||||
<div class="mb-0">{{ $t('1000thanks') }}</div>
|
||||
</b-col>
|
||||
<b-col cols="3" class="text-right d-none d-sm-none d-md-inline">
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
</fade-transition>
|
||||
</div>
|
||||
<content-footer v-if="!$route.meta.hideFooter"></content-footer>
|
||||
<session-logout-timeout @logout="logout"></session-logout-timeout>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -39,6 +40,7 @@
|
||||
<script>
|
||||
import Navbar from '@/components/Menu/Navbar.vue'
|
||||
import Sidebar from '@/components/Menu/Sidebar.vue'
|
||||
import SessionLogoutTimeout from '@/components/SessionLogoutTimeout.vue'
|
||||
import { logout, transactionsQuery } from '@/graphql/queries'
|
||||
import ContentFooter from '@/components/ContentFooter.vue'
|
||||
import { FadeTransition } from 'vue2-transitions'
|
||||
@ -49,6 +51,7 @@ export default {
|
||||
components: {
|
||||
Navbar,
|
||||
Sidebar,
|
||||
SessionLogoutTimeout,
|
||||
ContentFooter,
|
||||
FadeTransition,
|
||||
},
|
||||
|
||||
@ -125,7 +125,9 @@
|
||||
"gdd_per_link": {
|
||||
"choose-amount": "Wähle einen Betrag aus, welchen du per Link versenden möchtest. Du kannst auch noch eine Nachricht eintragen. Beim Klick „Jetzt generieren“ wird ein Link erstellt, den du versenden kannst.",
|
||||
"copy": "kopieren",
|
||||
"copy-with-text": "Link und Text kopieren",
|
||||
"created": "Der Link wurde erstellt!",
|
||||
"credit-your-gradido": "Damit die Gradido gutgeschrieben werden können, klicke auf den Link!",
|
||||
"decay-14-day": "Vergänglichkeit für 14 Tage",
|
||||
"delete-the-link": "Den Link löschen?",
|
||||
"deleted": "Der Link wurde gelöscht!",
|
||||
@ -133,6 +135,7 @@
|
||||
"has-account": "Du besitzt bereits ein Gradido Konto?",
|
||||
"header": "Gradidos versenden per Link",
|
||||
"isFree": "Gradido ist weltweit kostenfrei.",
|
||||
"link-and-text-copied": "Der Link und deine Nachricht wurden in die Zwischenablage kopiert. Du kannst ihn jetzt in eine E-Mail oder Nachricht einfügen.",
|
||||
"link-copied": "Link wurde in die Zwischenablage kopiert. Du kannst ihn jetzt in eine E-Mail oder Nachricht einfügen.",
|
||||
"link-deleted": "Der Link wurde am {date} gelöscht.",
|
||||
"link-expired": "Der Link ist nicht mehr gültig. Die Gültigkeit ist am {date} abgelaufen.",
|
||||
@ -149,7 +152,8 @@
|
||||
"redeemed-title": "eingelöst",
|
||||
"to-login": "Log dich ein",
|
||||
"to-register": "Registriere ein neues Konto.",
|
||||
"validUntil": "Gültig bis"
|
||||
"validUntil": "Gültig bis",
|
||||
"validUntilDate": "Der Link ist bis zum {date} gültig."
|
||||
},
|
||||
"gdt": {
|
||||
"calculation": "Berechnung der Gradido Transform",
|
||||
@ -201,6 +205,12 @@
|
||||
"qrCode": "QR Code",
|
||||
"send_gdd": "GDD versenden",
|
||||
"send_per_link": "GDD versenden per Link",
|
||||
"session": {
|
||||
"extend": "Angemeldet bleiben",
|
||||
"lightText": "Wenn du länger als 10 Minuten keine Aktion getätigt hast, wirst du aus Sicherheitsgründen abgemeldet.",
|
||||
"logoutIn": "Abmelden in ",
|
||||
"warningText": "Bist du noch da?"
|
||||
},
|
||||
"settings": {
|
||||
"language": {
|
||||
"changeLanguage": "Sprache ändern",
|
||||
@ -278,6 +288,5 @@
|
||||
"send_you": "sendet dir"
|
||||
},
|
||||
"via_link": "über einen Link",
|
||||
"welcome": "Willkommen",
|
||||
"WelcomeBy": "bei {name}"
|
||||
"welcome": "Willkommen in der Gemeinschaft"
|
||||
}
|
||||
|
||||
@ -125,7 +125,9 @@
|
||||
"gdd_per_link": {
|
||||
"choose-amount": "Select an amount that you would like to send via link. You can also enter a message. Click 'Generate now' to create a link that you can share.",
|
||||
"copy": "copy",
|
||||
"copy-with-text": "Copy link and text",
|
||||
"created": "Link was created!",
|
||||
"credit-your-gradido": "For the Gradido to be credited, click on the link!",
|
||||
"decay-14-day": "Decay for 14 days",
|
||||
"delete-the-link": "Delete the link?",
|
||||
"deleted": "The link was deleted!",
|
||||
@ -133,6 +135,7 @@
|
||||
"has-account": "You already have a Gradido account?",
|
||||
"header": "Send Gradidos via link",
|
||||
"isFree": "Gradido is free of charge worldwide.",
|
||||
"link-and-text-copied": "The link and your message have been copied to the clipboard. You can now include it in an email or message.",
|
||||
"link-copied": "Link has been copied to the clipboard. You can now paste it into an email or message.",
|
||||
"link-deleted": "The link was deleted on {date}.",
|
||||
"link-expired": "The link is no longer valid. The validity expired on {date}.",
|
||||
@ -149,7 +152,8 @@
|
||||
"redeemed-title": "redeemed",
|
||||
"to-login": "Log in",
|
||||
"to-register": "Register a new account.",
|
||||
"validUntil": "Valid until"
|
||||
"validUntil": "Valid until",
|
||||
"validUntilDate": "The link is valid until {date}."
|
||||
},
|
||||
"gdt": {
|
||||
"calculation": "Calculation of Gradido Transform",
|
||||
@ -201,6 +205,12 @@
|
||||
"qrCode": "QR Code",
|
||||
"send_gdd": "GDD send",
|
||||
"send_per_link": "GDD send via link",
|
||||
"session": {
|
||||
"extend": "Stay logged in",
|
||||
"lightText": "If you have not performed any action for more than 10 minutes, you will be logged out for security reasons.",
|
||||
"logoutIn": "Log out in ",
|
||||
"warningText": "Are you still there?"
|
||||
},
|
||||
"settings": {
|
||||
"language": {
|
||||
"changeLanguage": "Change language",
|
||||
@ -278,6 +288,5 @@
|
||||
"send_you": "wants to send you"
|
||||
},
|
||||
"via_link": "via Link",
|
||||
"welcome": "Welcome",
|
||||
"WelcomeBy": "by {name}"
|
||||
"welcome": "Welcome to the community"
|
||||
}
|
||||
|
||||
@ -18,6 +18,8 @@ import 'vue-loading-overlay/dist/vue-loading.css'
|
||||
|
||||
import VueApollo from 'vue-apollo'
|
||||
|
||||
import VueTimers from 'vue-timers'
|
||||
|
||||
export default {
|
||||
install(Vue) {
|
||||
Vue.use(GlobalComponents)
|
||||
@ -29,5 +31,6 @@ export default {
|
||||
Vue.use(FlatPickr)
|
||||
Vue.use(Loading)
|
||||
Vue.use(VueApollo)
|
||||
Vue.use(VueTimers)
|
||||
},
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import Vuex from 'vuex'
|
||||
import createPersistedState from 'vuex-persistedstate'
|
||||
import { localeChanged } from 'vee-validate'
|
||||
import i18n from '@/i18n.js'
|
||||
import jwtDecode from 'jwt-decode'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
@ -26,6 +27,11 @@ export const mutations = {
|
||||
},
|
||||
token: (state, token) => {
|
||||
state.token = token
|
||||
if (token) {
|
||||
state.tokenTime = jwtDecode(token).exp
|
||||
} else {
|
||||
state.tokenTime = null
|
||||
}
|
||||
},
|
||||
newsletterState: (state, newsletterState) => {
|
||||
state.newsletterState = newsletterState
|
||||
@ -85,6 +91,7 @@ try {
|
||||
lastName: '',
|
||||
// username: '',
|
||||
token: null,
|
||||
tokenTime: null,
|
||||
isAdmin: false,
|
||||
newsletterState: null,
|
||||
hasElopage: false,
|
||||
|
||||
@ -3,6 +3,7 @@ import Vuex from 'vuex'
|
||||
import Vue from 'vue'
|
||||
import i18n from '@/i18n.js'
|
||||
import { localeChanged } from 'vee-validate'
|
||||
import jwtDecode from 'jwt-decode'
|
||||
|
||||
jest.mock('vuex')
|
||||
jest.mock('@/i18n.js')
|
||||
@ -11,6 +12,11 @@ jest.mock('vee-validate', () => {
|
||||
localeChanged: jest.fn(),
|
||||
}
|
||||
})
|
||||
jest.mock('jwt-decode', () => {
|
||||
return jest.fn(() => {
|
||||
return { exp: '1234' }
|
||||
})
|
||||
})
|
||||
|
||||
i18n.locale = 'blubb'
|
||||
|
||||
@ -59,6 +65,25 @@ describe('Vuex store', () => {
|
||||
token(state, '1234')
|
||||
expect(state.token).toEqual('1234')
|
||||
})
|
||||
|
||||
describe('token has a value', () => {
|
||||
it('sets the state of tokenTime', () => {
|
||||
const state = { token: null, tokenTime: null }
|
||||
token(state, 'token')
|
||||
expect(jwtDecode).toBeCalledWith('token')
|
||||
expect(state.tokenTime).toEqual('1234')
|
||||
})
|
||||
})
|
||||
|
||||
describe('token has null value', () => {
|
||||
it('sets the state of tokenTime to null', () => {
|
||||
jest.clearAllMocks()
|
||||
const state = { token: null, tokenTime: '123' }
|
||||
token(state, null)
|
||||
expect(jwtDecode).not.toBeCalled()
|
||||
expect(state.tokenTime).toEqual(null)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('firstName', () => {
|
||||
|
||||
@ -8,6 +8,7 @@ import * as rules from 'vee-validate/dist/rules'
|
||||
import { messages } from 'vee-validate/dist/locale/en.json'
|
||||
|
||||
import RegeneratorRuntime from 'regenerator-runtime'
|
||||
import VueTimers from 'vue-timers'
|
||||
|
||||
import VueMoment from 'vue-moment'
|
||||
|
||||
@ -46,6 +47,7 @@ global.localVue.use(Vuex)
|
||||
global.localVue.use(IconsPlugin)
|
||||
global.localVue.use(RegeneratorRuntime)
|
||||
global.localVue.use(VueMoment)
|
||||
global.localVue.use(VueTimers)
|
||||
global.localVue.component('validation-provider', ValidationProvider)
|
||||
global.localVue.component('validation-observer', ValidationObserver)
|
||||
// global.localVue.directive('click-outside', clickOutside)
|
||||
|
||||
@ -9821,6 +9821,11 @@ jsprim@^1.2.2:
|
||||
json-schema "0.2.3"
|
||||
verror "1.10.0"
|
||||
|
||||
jwt-decode@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59"
|
||||
integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==
|
||||
|
||||
killable@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"
|
||||
@ -14472,6 +14477,11 @@ vue-template-es2015-compiler@^1.6.0, vue-template-es2015-compiler@^1.9.0:
|
||||
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
|
||||
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
|
||||
|
||||
vue-timers@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/vue-timers/-/vue-timers-2.0.4.tgz#7e1c443abf2109db5eeab6e62b0f5a47e94cf70b"
|
||||
integrity sha512-QOEVdO4V4o9WjFG6C0Kn9tfdTeeECjqvEQozcQlfL1Tn8v0qx4uUPhTYoc1+s6qoJnSbu8f68x8+nm1ZEir0kw==
|
||||
|
||||
vue2-transitions@^0.2.3:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/vue2-transitions/-/vue2-transitions-0.2.3.tgz#69c9d75b1db05f231b80980c03459d68490ba27d"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido",
|
||||
"version": "1.10.0",
|
||||
"version": "1.10.1",
|
||||
"description": "Gradido",
|
||||
"main": "index.js",
|
||||
"repository": "git@github.com:gradido/gradido.git",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user