Merge branch 'master' into apollo_saveUserOnCreate

This commit is contained in:
einhornimmond 2021-10-06 12:15:20 +02:00
commit a531b4671f
68 changed files with 7776 additions and 194 deletions

View File

@ -344,7 +344,7 @@ jobs:
report_name: Coverage Frontend
type: lcov
result_path: ./coverage/lcov.info
min_coverage: 69
min_coverage: 73
token: ${{ github.token }}
##############################################################################
@ -470,7 +470,7 @@ jobs:
run: echo "::set-output name=id::$(docker network ls | grep github_network | awk '{ print $1 }')"
id: network
- name: Start database migration
run: docker run --network ${{ steps.network.outputs.id }} --name=database --env NODE_ENV=production --env DB_HOST=mariadb -d gradido/database:production_up
run: docker run --network ${{ steps.network.outputs.id }} --name=database --env NODE_ENV=production --env DB_HOST=mariadb --env DB_DATABASE=gradido_community_test -d gradido/database:production_up
- name: get database migration container id
run: echo "::set-output name=id::$(docker container ls | grep database | awk '{ print $1 }')"
id: database_container

2
backend/.gitignore vendored
View File

@ -1,7 +1,7 @@
/node_modules/
/.env
/build/
package-json.lock
coverage
# emacs
*~

7120
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,9 +2,6 @@ import { ArgsType, Field } from 'type-graphql'
@ArgsType()
export default class UpdateUserInfosArgs {
@Field(() => String)
email!: string
@Field({ nullable: true })
firstName?: string
@ -28,4 +25,7 @@ export default class UpdateUserInfosArgs {
@Field({ nullable: true })
passwordNew?: string
@Field({ nullable: true })
coinanimation?: boolean
}

View File

@ -1,13 +1,16 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { AuthChecker } from 'type-graphql'
import decode from '../jwt/decode'
import { apiGet } from '../apis/HttpRequest'
import CONFIG from '../config'
import encode from '../jwt/encode'
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
export const isAuthorized: AuthChecker<any> = async ({ root, args, context, info }, roles) => {
import CONFIG from '../../config'
import { apiGet } from '../../apis/HttpRequest'
import decode from '../../jwt/decode'
import encode from '../../jwt/encode'
const isAuthorized: AuthChecker<any> = async (
{ /* root, args, */ context /*, info */ } /*, roles */,
) => {
if (context.token) {
const decoded = decode(context.token)
if (decoded.sessionId && decoded.sessionId !== 0) {
@ -22,3 +25,5 @@ export const isAuthorized: AuthChecker<any> = async ({ root, args, context, info
}
throw new Error('401 Unauthorized')
}
export default isAuthorized

View File

@ -0,0 +1,5 @@
enum Setting {
COIN_ANIMATION = 'coinanimation',
}
export { Setting }

View File

@ -69,6 +69,9 @@ export class User {
@Field(() => Number)
publisherId: number
@Field(() => Boolean)
coinanimation: boolean
@Field(() => KlickTipp)
klickTipp: KlickTipp
}

View File

@ -3,7 +3,7 @@
import { Resolver, Query, Ctx, Authorized } from 'type-graphql'
import { getCustomRepository } from 'typeorm'
import { Balance } from '../models/Balance'
import { Balance } from '../model/Balance'
import { BalanceRepository } from '../../typeorm/repository/Balance'
import { UserRepository } from '../../typeorm/repository/User'
import { calculateDecay } from '../../util/decay'

View File

@ -4,8 +4,8 @@
import { Resolver, Query, Args, Ctx, Authorized } from 'type-graphql'
import { getCustomRepository } from 'typeorm'
import CONFIG from '../../config'
import { GdtEntryList } from '../models/GdtEntryList'
import Paginated from '../args/Paginated'
import { GdtEntryList } from '../model/GdtEntryList'
import Paginated from '../arg/Paginated'
import { apiGet } from '../../apis/HttpRequest'
import { UserRepository } from '../../typeorm/repository/User'
import { Order } from '../enum/Order'
@ -30,7 +30,6 @@ export class GdtResolver {
if (!resultGDT.success) {
throw new Error(resultGDT.data)
}
return new GdtEntryList(resultGDT.data)
}
}

View File

@ -8,7 +8,7 @@ import {
unsubscribe,
signIn,
} from '../../apis/KlicktippController'
import SubscribeNewsletterArgs from '../args/SubscribeNewsletterArgs'
import SubscribeNewsletterArgs from '../arg/SubscribeNewsletterArgs'
@Resolver()
export class KlicktippResolver {

View File

@ -6,11 +6,11 @@ import { getCustomRepository } from 'typeorm'
import CONFIG from '../../config'
import { Transaction } from '../models/Transaction'
import { TransactionList } from '../models/TransactionList'
import { Transaction } from '../model/Transaction'
import { TransactionList } from '../model/TransactionList'
import TransactionSendArgs from '../args/TransactionSendArgs'
import Paginated from '../args/Paginated'
import TransactionSendArgs from '../arg/TransactionSendArgs'
import Paginated from '../arg/Paginated'
import { Order } from '../enum/Order'
@ -50,7 +50,7 @@ async function calculateAndAddDecayTransactions(
const transactionIndiced: dbTransaction[] = []
transactions.forEach((transaction: dbTransaction) => {
transactionIndiced[transaction.id] = transaction
if (transaction.transactionTypeId === 2) {
if (transaction.transactionTypeId === TransactionTypeId.SEND) {
involvedUserIds.push(transaction.transactionSendCoin.userId)
involvedUserIds.push(transaction.transactionSendCoin.recipiantUserId)
}
@ -97,7 +97,7 @@ async function calculateAndAddDecayTransactions(
}
}
// sender or receiver when user has sended money
// sender or receiver when user has sent money
// group name if creation
// type: gesendet / empfangen / geschöpft
// transaktion nr / id
@ -227,7 +227,7 @@ export class TransactionResolver {
email: userEntity.email,
})
if (!resultGDTSum.success) throw new Error(resultGDTSum.data)
transactions.gdtSum = resultGDTSum.data.sum
transactions.gdtSum = resultGDTSum.data.sum || 0
// get balance
const balanceRepository = getCustomRepository(BalanceRepository)

View File

@ -4,24 +4,28 @@
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
import { from_hex as fromHex } from 'libsodium-wrappers'
import CONFIG from '../../config'
import { CheckUsernameResponse } from '../models/CheckUsernameResponse'
import { LoginViaVerificationCode } from '../models/LoginViaVerificationCode'
import { SendPasswordResetEmailResponse } from '../models/SendPasswordResetEmailResponse'
import { UpdateUserInfosResponse } from '../models/UpdateUserInfosResponse'
import { User } from '../models/User'
import { CheckUsernameResponse } from '../model/CheckUsernameResponse'
import { LoginViaVerificationCode } from '../model/LoginViaVerificationCode'
import { SendPasswordResetEmailResponse } from '../model/SendPasswordResetEmailResponse'
import { UpdateUserInfosResponse } from '../model/UpdateUserInfosResponse'
import { User } from '../model/User'
import { User as DbUser } from '../../typeorm/entity/User'
import encode from '../../jwt/encode'
import ChangePasswordArgs from '../args/ChangePasswordArgs'
import CheckUsernameArgs from '../args/CheckUsernameArgs'
import CreateUserArgs from '../args/CreateUserArgs'
import UnsecureLoginArgs from '../args/UnsecureLoginArgs'
import UpdateUserInfosArgs from '../args/UpdateUserInfosArgs'
import ChangePasswordArgs from '../arg/ChangePasswordArgs'
import CheckUsernameArgs from '../arg/CheckUsernameArgs'
import CreateUserArgs from '../arg/CreateUserArgs'
import UnsecureLoginArgs from '../arg/UnsecureLoginArgs'
import UpdateUserInfosArgs from '../arg/UpdateUserInfosArgs'
import { apiPost, apiGet } from '../../apis/HttpRequest'
import {
klicktippRegistrationMiddleware,
klicktippNewsletterStateMiddleware,
} from '../../middleware/klicktippMiddleware'
import { CheckEmailResponse } from '../models/CheckEmailResponse'
import { CheckEmailResponse } from '../model/CheckEmailResponse'
import { getCustomRepository } from 'typeorm'
import { UserSettingRepository } from '../../typeorm/repository/UserSettingRepository'
import { Setting } from '../enum/Setting'
import { UserRepository } from '../../typeorm/repository/User'
@Resolver()
export class UserResolver {
@ -40,8 +44,19 @@ export class UserResolver {
key: 'token',
value: encode(result.data.session_id, result.data.user.public_hex),
})
const user = new User(result.data.user)
// read additional settings from settings table
const userRepository = getCustomRepository(UserRepository)
const userEntity = await userRepository.findByPubkeyHex(user.pubkey)
return new User(result.data.user)
const userSettingRepository = getCustomRepository(UserSettingRepository)
const coinanimation = await userSettingRepository
.readBoolean(userEntity.id, Setting.COIN_ANIMATION)
.catch((error) => {
throw new Error(error)
})
user.coinanimation = coinanimation
return user
}
@Query(() => LoginViaVerificationCode)
@ -142,7 +157,6 @@ export class UserResolver {
async updateUserInfos(
@Args()
{
email,
firstName,
lastName,
description,
@ -151,12 +165,12 @@ export class UserResolver {
publisherId,
password,
passwordNew,
coinanimation,
}: UpdateUserInfosArgs,
@Ctx() context: any,
): Promise<UpdateUserInfosResponse> {
const payload = {
session_id: context.sessionId,
email,
update: {
'User.first_name': firstName || undefined,
'User.last_name': lastName || undefined,
@ -168,9 +182,42 @@ export class UserResolver {
'User.password_old': password || undefined,
},
}
let response: UpdateUserInfosResponse | undefined
if (
firstName ||
lastName ||
description ||
username ||
language ||
publisherId ||
passwordNew ||
password
) {
const result = await apiPost(CONFIG.LOGIN_API_URL + 'updateUserInfos', payload)
if (!result.success) throw new Error(result.data)
return new UpdateUserInfosResponse(result.data)
response = new UpdateUserInfosResponse(result.data)
}
if (coinanimation !== undefined) {
// load user and balance
const userRepository = getCustomRepository(UserRepository)
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
const userSettingRepository = getCustomRepository(UserSettingRepository)
userSettingRepository
.setOrUpdate(userEntity.id, Setting.COIN_ANIMATION, coinanimation.toString())
.catch((error) => {
throw new Error(error)
})
if (!response) {
response = new UpdateUserInfosResponse({ valid_values: 1 })
} else {
response.validValues++
}
}
if (!response) {
throw new Error('no valid response')
}
return response
}
@Query(() => CheckUsernameResponse)

View File

@ -1,19 +0,0 @@
import { UserResolver } from './UserResolver'
import { BalanceResolver } from './BalanceResolver'
import { GdtResolver } from './GdtResolver'
import { TransactionResolver } from './TransactionResolver'
import { KlicktippResolver } from './KlicktippResolver'
import { NonEmptyArray } from 'type-graphql'
export { UserResolver, BalanceResolver, GdtResolver, TransactionResolver, KlicktippResolver }
// eslint-disable-next-line @typescript-eslint/ban-types
const resolvers = (): NonEmptyArray<Function> => [
UserResolver,
BalanceResolver,
GdtResolver,
TransactionResolver,
KlicktippResolver,
]
export default resolvers

View File

@ -1,12 +1,12 @@
import { GraphQLSchema } from 'graphql'
import { buildSchema } from 'type-graphql'
import path from 'path'
import resolvers from './resolvers'
import { isAuthorized } from '../auth/auth'
import isAuthorized from './directive/isAuthorized'
const schema = async (): Promise<GraphQLSchema> => {
return buildSchema({
resolvers: resolvers(),
resolvers: [path.join(__dirname, 'resolver', `*.{js,ts}`)],
authChecker: isAuthorized,
})
}

View File

@ -22,7 +22,7 @@ import schema from './graphql/schema'
// TODO implement
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
const DB_VERSION = '0001-init_db'
const DB_VERSION = '0002-add_settings'
async function main() {
// open mysql connection

View File

@ -1,6 +1,6 @@
import { MiddlewareFn } from 'type-graphql'
import { signIn, getKlickTippUser } from '../apis/KlicktippController'
import { KlickTipp } from '../graphql/models/KlickTipp'
import { KlickTipp } from '../graphql/model/KlickTipp'
import CONFIG from '../config/index'
export const klicktippRegistrationMiddleware: MiddlewareFn = async (

View File

@ -1,6 +1,5 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
// import { Group } from "./Group"
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'
import { UserSetting } from './UserSetting'
// Moriz: I do not like the idea of having two user tables
@Entity('state_users')
@ -28,4 +27,7 @@ export class User extends BaseEntity {
@Column()
disabled: boolean
@OneToMany(() => UserSetting, (userSetting) => userSetting.user)
settings: UserSetting[]
}

View File

@ -0,0 +1,20 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'
import { User } from './User'
@Entity()
export class UserSetting extends BaseEntity {
@PrimaryGeneratedColumn()
id: number
@Column()
userId: number
@ManyToOne(() => User, (user) => user.settings)
user: User
@Column()
key: string
@Column()
value: string
}

View File

@ -1,11 +1,8 @@
import { getConnection } from 'typeorm'
import { Migration } from './entity/Migration'
const getDBVersion = async (): Promise<string | null> => {
const connection = getConnection()
const migrations = connection.getRepository(Migration)
try {
const dbVersion = await migrations.findOne({ order: { version: 'DESC' } })
const dbVersion = await Migration.findOne({ order: { version: 'DESC' } })
return dbVersion ? dbVersion.fileName : null
} catch (error) {
return null

View File

@ -4,8 +4,6 @@ import { Balance } from '../entity/Balance'
@EntityRepository(Balance)
export class BalanceRepository extends Repository<Balance> {
findByUser(userId: number): Promise<Balance | undefined> {
return this.createQueryBuilder('balance')
.where('balance.userId = :userId', { userId })
.getOneOrFail()
return this.createQueryBuilder('balance').where('balance.userId = :userId', { userId }).getOne()
}
}

View File

@ -0,0 +1,36 @@
import { EntityRepository, Repository } from 'typeorm'
import { UserSetting } from '../entity/UserSetting'
import { Setting } from '../../graphql/enum/Setting'
import { isStringBoolean } from '../../util/validate'
@EntityRepository(UserSetting)
export class UserSettingRepository extends Repository<UserSetting> {
async setOrUpdate(userId: number, key: Setting, value: string): Promise<UserSetting> {
switch (key) {
case Setting.COIN_ANIMATION:
if (!isStringBoolean(value)) {
throw new Error("coinanimation value isn't boolean")
}
break
default:
throw new Error("key isn't defined: " + key)
}
let entity = await this.findOne({ userId: userId, key: key })
if (!entity) {
entity = new UserSetting()
entity.userId = userId
entity.key = key
}
entity.value = value
return this.save(entity)
}
async readBoolean(userId: number, key: Setting): Promise<boolean> {
const entity = await this.findOne({ userId: userId, key: key })
if (!entity || !isStringBoolean(entity.value)) {
return true
}
return entity.value.toLowerCase() === 'true'
}
}

View File

@ -1,6 +1,29 @@
import { calculateDecay } from './decay'
import { decayFormula, calculateDecay } from './decay'
describe('utils/decay', () => {
describe('decayFormula', () => {
it('has base 0.99999997802044727', async () => {
const amount = 1.0
const seconds = 1
expect(await decayFormula(amount, seconds)).toBe(0.99999997802044727)
})
// Not sure if the following skiped tests make sence!?
it.skip('has negative decay?', async () => {
const amount = -1.0
const seconds = 1
expect(await decayFormula(amount, seconds)).toBe(-0.99999997802044727)
})
it.skip('has correct backward calculation', async () => {
const amount = 1.0
const seconds = -1
expect(await decayFormula(amount, seconds)).toBe(1.0000000219795533)
})
it.skip('has correct forward calculation', async () => {
const amount = 1.000000219795533
const seconds = 1
expect(await decayFormula(amount, seconds)).toBe(1.0)
})
})
it.skip('has base 0.99999997802044727', async () => {
const now = new Date()
now.setSeconds(1)

View File

@ -1,9 +1,9 @@
import { getCustomRepository } from 'typeorm'
import { Decay } from '../graphql/models/Decay'
import { Decay } from '../graphql/model/Decay'
import { TransactionRepository } from '../typeorm/repository/Transaction'
function decayFormula(amount: number, durationInSeconds: number): number {
return amount * Math.pow(0.99999997802044727, durationInSeconds)
function decayFormula(amount: number, seconds: number): number {
return amount * Math.pow(0.99999997802044727, seconds) // This number represents 50% decay a year
}
async function calculateDecay(amount: number, from: Date, to: Date): Promise<number> {
@ -58,4 +58,4 @@ async function calculateDecayWithInterval(
return result
}
export { calculateDecay, calculateDecayWithInterval }
export { decayFormula, calculateDecay, calculateDecayWithInterval }

View File

@ -0,0 +1,9 @@
function isStringBoolean(value: string): boolean {
const lowerValue = value.toLowerCase()
if (lowerValue === 'true' || lowerValue === 'false') {
return true
}
return false
}
export { isStringBoolean }

View File

@ -218,7 +218,7 @@
"@babel/highlight@^7.10.4", "@babel/highlight@^7.14.5":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9"
resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz"
integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==
dependencies:
"@babel/helper-validator-identifier" "^7.14.5"
@ -4802,7 +4802,7 @@ semver-diff@^3.1.1:
semver@7.x, semver@^7.2.1, semver@^7.3.2, semver@^7.3.5:
version "7.3.5"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
resolved "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz"
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
dependencies:
lru-cache "^6.0.0"
@ -5715,9 +5715,9 @@ yargs@^16.0.0, yargs@^16.2.0:
yargs-parser "^20.2.2"
yargs@^17.0.1:
version "17.2.0"
resolved "https://registry.npmjs.org/yargs/-/yargs-17.2.0.tgz"
integrity sha512-UPeZv4h9Xv510ibpt5rdsUNzgD78nMa1rhxxCgvkKiq06hlKCEHJLiJ6Ub8zDg/wR6hedEI6ovnd2vCvJ4nusA==
version "17.1.1"
resolved "https://registry.npmjs.org/yargs/-/yargs-17.1.1.tgz"
integrity sha512-c2k48R0PwKIqKhPMWjeiF6y2xY/gPMUlro0sgxqXpbOIohWiLNXWslsootttv7E1e73QPAMQSg5FeySbVcpsPQ==
dependencies:
cliui "^7.0.2"
escalade "^3.1.1"

View File

@ -0,0 +1,26 @@
/* FIRST MIGRATION
*
* This migration is special since it takes into account that
* the database can be setup already but also may not be.
* Therefore you will find all `CREATE TABLE` statements with
* a `IF NOT EXISTS`, all `INSERT` with an `IGNORE` and in the
* downgrade function all `DROP TABLE` with a `IF EXISTS`.
* This ensures compatibility for existing or non-existing
* databases.
*/
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`
CREATE TABLE IF NOT EXISTS \`user_setting\` (
\`id\` int(10) unsigned NOT NULL AUTO_INCREMENT,
\`userId\` int(11) NOT NULL,
\`key\` varchar(255) NOT NULL,
\`value\` varchar(255) NOT NULL,
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// write downgrade logic as parameter of queryFn
await queryFn(`DROP TABLE IF EXISTS \`user_setting\`;`)
}

View File

@ -25,12 +25,6 @@ export default async (): Promise<void> => {
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;`)
// Create Database `gradido_community_test` for tests
await con.query(`
CREATE DATABASE IF NOT EXISTS ${CONFIG.DB_DATABASE}_test
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;`)
// Check if old migration table is present, delete if needed
const [rows] = await con.query(`SHOW TABLES FROM \`${CONFIG.DB_DATABASE}\` LIKE 'migrations';`)
if ((<RowDataPacket>rows).length > 0) {

View File

@ -43,11 +43,13 @@ services:
# DATABASE ##############################################
########################################################
database:
# we always run on prouction here since else the service lingers
# we always run on production here since else the service lingers
# feel free to change this behaviour if it seems useful
#image: gradido/database:test_up
#build:
# target: test_up
# Due to problems with the volume caching the built files
# we changed this to test build. This keeps the service running.
image: gradido/database:test_up
build:
target: test_up
#networks:
# - external-net
# - internal-net

View File

@ -222,7 +222,6 @@ with:
```json
{
"session_id": -127182,
"email": "max.musterman@gmail.de",
"update": {
"User.first_name": "Max",
"User.last_name" : "Musterman",

View File

@ -1,7 +1,7 @@
<template>
<div id="app" class="font-sans text-gray-800">
<div class="">
<particles-bg type="custom" :config="config" :bg="true" />
<particles-bg v-if="$store.state.coinanimation" type="custom" :config="config" :bg="true" />
<component :is="$route.meta.requiresAuth ? 'DashboardLayout' : 'AuthLayoutGDD'" />
</div>
</div>

View File

@ -38,7 +38,7 @@
</b-row>
<!-- Message-->
<b-row v-if="comment && gdtEntryType !== 7">
<b-row v-if="comment && !isGlobalModificator">
<b-col cols="6" class="text-right">
{{ $t('form.memo') }}
</b-col>
@ -72,6 +72,7 @@
</template>
<script>
import TransactionCollapse from './TransactionCollapse.vue'
import { GdtEntryType } from '../graphql/enums'
export default {
name: 'Transaction',
@ -82,42 +83,52 @@ export default {
amount: { type: Number },
date: { type: String },
comment: { type: String },
gdtEntryType: { type: Number, default: 1 },
gdtEntryType: { type: String, default: GdtEntryType.FORM },
factor: { type: Number },
gdt: { type: Number },
},
computed: {
isGlobalModificator: function () {
return this.gdtEntryType === GdtEntryType.GLOBAL_MODIFICATOR
},
},
methods: {
getLinesByType(givenType) {
if (givenType === 2 || givenType === 3 || givenType === 5 || givenType === 6) givenType = 1
const linesByType = {
1: {
icon: 'heart',
iconclasses: 'gradido-global-color-accent m-mb-1 font2em',
description: this.$t('gdt.contribution'),
descriptiontext: this.$n(this.amount, 'decimal') + ' €',
credittext: this.$n(this.gdt, 'decimal') + ' GDT',
},
4: {
icon: 'person-check',
iconclasses: 'gradido-global-color-accent m-mb-1 font2em',
description: this.$t('gdt.recruited-member'),
descriptiontext: '5%',
credittext: this.$n(this.amount, 'decimal') + ' GDT',
},
7: {
icon: 'gift',
iconclasses: 'gradido-global-color-accent m-mb-1 font2em',
description: this.$t('gdt.gdt-received'),
descriptiontext: this.comment,
credittext: this.$n(this.gdt, 'decimal') + ' GDT',
},
switch (givenType) {
case GdtEntryType.FORM:
case GdtEntryType.CVS:
case GdtEntryType.ELOPAGE:
case GdtEntryType.DIGISTORE:
case GdtEntryType.CVS2: {
return {
icon: 'heart',
iconclasses: 'gradido-global-color-accent m-mb-1 font2em',
description: this.$t('gdt.contribution'),
descriptiontext: this.$n(this.amount, 'decimal') + ' €',
credittext: this.$n(this.gdt, 'decimal') + ' GDT',
}
}
case GdtEntryType.ELOPAGE_PUBLISHER: {
return {
icon: 'person-check',
iconclasses: 'gradido-global-color-accent m-mb-1 font2em',
description: this.$t('gdt.recruited-member'),
descriptiontext: '5%',
credittext: this.$n(this.amount, 'decimal') + ' GDT',
}
}
case GdtEntryType.GLOBAL_MODIFICATOR: {
return {
icon: 'gift',
iconclasses: 'gradido-global-color-accent m-mb-1 font2em',
description: this.$t('gdt.gdt-received'),
descriptiontext: this.comment,
credittext: this.$n(this.gdt, 'decimal') + ' GDT',
}
}
default:
throw new Error('no lines for this type: ' + givenType)
}
const type = linesByType[givenType]
if (type) return type
throw new Error('no lines for this type: ' + givenType)
},
},
}

View File

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils'
import TransactionCollapse from './TransactionCollapse'
import { GdtEntryType } from '../graphql/enums'
const localVue = global.localVue
@ -15,13 +16,13 @@ describe('TransactionCollapse', () => {
return mount(TransactionCollapse, { localVue, mocks, propsData })
}
describe('mount with gdtEntryType: 1', () => {
describe('mount with gdtEntryType: FORM', () => {
beforeEach(() => {
const propsData = {
amount: 100,
gdt: 110,
factor: 22,
gdtEntryType: 1,
gdtEntryType: GdtEntryType.FORM,
}
wrapper = Wrapper(propsData)
@ -31,8 +32,8 @@ describe('TransactionCollapse', () => {
expect(wrapper.find('div.gdt-transaction-collapse').exists()).toBeTruthy()
})
it('checks the prop gdtEntryType ', () => {
expect(wrapper.props().gdtEntryType).toBe(1)
it('checks the prop gdtEntryType', () => {
expect(wrapper.props().gdtEntryType).toBe('FORM')
})
it('renders the component collapse-header', () => {
@ -60,13 +61,13 @@ describe('TransactionCollapse', () => {
})
})
describe('mount with gdtEntryType: 7', () => {
describe('mount with gdtEntryType: GLOBAL_MODIFICATOR', () => {
beforeEach(() => {
const propsData = {
amount: 100,
gdt: 2200,
factor: 22,
gdtEntryType: 7,
gdtEntryType: GdtEntryType.GLOBAL_MODIFICATOR,
}
wrapper = Wrapper(propsData)
@ -76,8 +77,8 @@ describe('TransactionCollapse', () => {
expect(wrapper.find('div.gdt-transaction-collapse').exists()).toBeTruthy()
})
it('checks the prop gdtEntryType ', () => {
expect(wrapper.props().gdtEntryType).toBe(7)
it('checks the prop gdtEntryType', () => {
expect(wrapper.props().gdtEntryType).toBe('GLOBAL_MODIFICATOR')
})
it('renders the component collapse-header', () => {
@ -105,13 +106,13 @@ describe('TransactionCollapse', () => {
})
})
describe('mount with gdtEntryType: 4', () => {
describe('mount with gdtEntryType: ELOPAGE_PUBLISHER', () => {
beforeEach(() => {
const propsData = {
amount: 100,
gdt: 2200,
factor: 22,
gdtEntryType: 4,
gdtEntryType: GdtEntryType.ELOPAGE_PUBLISHER,
}
wrapper = Wrapper(propsData)
@ -121,8 +122,8 @@ describe('TransactionCollapse', () => {
expect(wrapper.find('div.gdt-transaction-collapse').exists()).toBeTruthy()
})
it('checks the prop gdtEntryType ', () => {
expect(wrapper.props().gdtEntryType).toBe(4)
it('checks the prop gdtEntryType', () => {
expect(wrapper.props().gdtEntryType).toBe('ELOPAGE_PUBLISHER')
})
it('renders the component collapse-header', () => {

View File

@ -23,58 +23,65 @@
</div>
</template>
<script>
import { GdtEntryType } from '../graphql/enums'
export default {
name: 'TransactionCollapse',
props: {
amount: { type: Number },
gdtEntryType: { type: Number, default: 1 },
gdtEntryType: { type: String, default: GdtEntryType.FORM },
factor: { type: Number },
gdt: { type: Number },
},
methods: {
getLinesByType(givenType) {
if (givenType === 2 || givenType === 3 || givenType === 5 || givenType === 6) givenType = 1
const linesByType = {
1: {
headline: this.$t('gdt.calculation'),
first: this.$t('gdt.factor'),
firstMath: this.factor + ' GDT pro €',
second: this.$t('gdt.formula'),
secondMath:
this.$n(this.amount, 'decimal') +
' € * ' +
this.factor +
' GDT / € = ' +
this.$n(this.gdt, 'decimal') +
' GDT',
},
4: {
headline: this.$t('gdt.publisher'),
first: null,
firstMath: null,
second: null,
secondMath: null,
},
7: {
headline: this.$t('gdt.conversion-gdt-euro'),
first: this.$t('gdt.raise'),
firstMath: this.factor * 100 + ' % ',
second: this.$t('gdt.conversion'),
secondMath:
this.$n(this.amount, 'decimal') +
' GDT * ' +
this.factor * 100 +
' % = ' +
this.$n(this.gdt, 'decimal') +
' GDT',
},
switch (givenType) {
case GdtEntryType.FORM:
case GdtEntryType.CVS:
case GdtEntryType.ELOPAGE:
case GdtEntryType.DIGISTORE:
case GdtEntryType.CVS2: {
return {
headline: this.$t('gdt.calculation'),
first: this.$t('gdt.factor'),
firstMath: this.factor + ' GDT pro €',
second: this.$t('gdt.formula'),
secondMath:
this.$n(this.amount, 'decimal') +
' € * ' +
this.factor +
' GDT / € = ' +
this.$n(this.gdt, 'decimal') +
' GDT',
}
}
case GdtEntryType.ELOPAGE_PUBLISHER: {
return {
headline: this.$t('gdt.publisher'),
first: null,
firstMath: null,
second: null,
secondMath: null,
}
}
case GdtEntryType.GLOBAL_MODIFICATOR: {
return {
headline: this.$t('gdt.conversion-gdt-euro'),
first: this.$t('gdt.raise'),
firstMath: this.factor * 100 + ' % ',
second: this.$t('gdt.conversion'),
secondMath:
this.$n(this.amount, 'decimal') +
' GDT * ' +
this.factor * 100 +
' % = ' +
this.$n(this.gdt, 'decimal') +
' GDT',
}
}
default:
throw new Error('no additional transaction info for this type: ' + givenType)
}
const type = linesByType[givenType]
if (type) return type
throw new Error('no additional transaction info for this type: ' + givenType)
},
},
}

View File

@ -0,0 +1,9 @@
export const GdtEntryType = {
FORM: 'FORM',
CVS: 'CVS',
ELOPAGE: 'ELOPAGE',
ELOPAGE_PUBLISHER: 'ELOPAGE_PUBLISHER',
DIGISTORE: 'DIGISTORE',
CVS2: 'CVS2',
GLOBAL_MODIFICATOR: 'GLOBAL_MODIFICATOR',
}

View File

@ -20,7 +20,6 @@ export const resetPassword = gql`
export const updateUserInfos = gql`
mutation(
$email: String!
$firstName: String
$lastName: String
$description: String
@ -28,9 +27,9 @@ export const updateUserInfos = gql`
$password: String
$passwordNew: String
$locale: String
$coinanimation: Boolean
) {
updateUserInfos(
email: $email
firstName: $firstName
lastName: $lastName
description: $description
@ -38,6 +37,7 @@ export const updateUserInfos = gql`
password: $password
passwordNew: $passwordNew
language: $locale
coinanimation: $coinanimation
) {
validValues
}

View File

@ -9,6 +9,7 @@ export const login = gql`
lastName
language
description
coinanimation
klickTipp {
newsletterState
}
@ -32,7 +33,7 @@ export const loginViaEmailVerificationCode = gql`
`
export const transactionsQuery = gql`
query($currentPage: Int = 1, $pageSize: Int = 25, $order: String = "DESC") {
query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
transactionList(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
gdtSum
count

View File

@ -105,6 +105,11 @@
"privacy_policy": "Datenschutzerklärung",
"send": "Senden",
"settings": {
"coinanimation": {
"coinanimation": "Münzanimation",
"False": "Münzanimation ausgeschaltet",
"True": "Münzanimation eingeschaltet"
},
"language": {
"changeLanguage": "Sprache ändern",
"de": "Deutsch",

View File

@ -105,6 +105,11 @@
"privacy_policy": "Privacy policy",
"send": "Send",
"settings": {
"coinanimation": {
"coinanimation": "Coin animation",
"False": "Coin animation disabled",
"True": "Coin animation enabled"
},
"language": {
"changeLanguage": "Change language",
"de": "Deutsch",

View File

@ -0,0 +1,155 @@
import router from './router'
import NotFound from '@/views/NotFoundPage.vue'
describe('router', () => {
describe('options', () => {
const { options } = router
const { scrollBehavior, routes } = options
it('has "/vue" as base', () => {
expect(options).toEqual(
expect.objectContaining({
base: '/vue',
}),
)
})
it('has "active" as linkActiveClass', () => {
expect(options).toEqual(
expect.objectContaining({
linkActiveClass: 'active',
}),
)
})
it('has "history" as mode', () => {
expect(options).toEqual(
expect.objectContaining({
mode: 'history',
}),
)
})
describe('scroll behavior', () => {
it('returns save position when given', () => {
expect(scrollBehavior({}, {}, 'given')).toBe('given')
})
it('returns selector when hash is given', () => {
expect(scrollBehavior({ hash: '#to' }, {})).toEqual({ selector: '#to' })
})
it('returns top left coordinates as default', () => {
expect(scrollBehavior({}, {})).toEqual({ x: 0, y: 0 })
})
})
describe('register page', () => {
it('is not present', () => {
expect(routes.find((r) => r.path === '/register')).toBe(undefined)
})
})
describe('routes', () => {
it('has "/login" as default', () => {
expect(routes.find((r) => r.path === '/').redirect()).toEqual({ path: '/login' })
})
it('has ten routes defined', () => {
expect(routes).toHaveLength(10)
})
describe('overview', () => {
it('requires authorization', () => {
expect(routes.find((r) => r.path === '/overview').meta.requiresAuth).toBeTruthy()
})
it('loads the "Overview" component', async () => {
const component = await routes.find((r) => r.path === '/overview').component()
expect(component.default.name).toBe('Overview')
})
})
describe('profile', () => {
it('requires authorization', () => {
expect(routes.find((r) => r.path === '/profile').meta.requiresAuth).toBeTruthy()
})
it('loads the "UserProfile" component', async () => {
const component = await routes.find((r) => r.path === '/profile').component()
expect(component.default.name).toBe('UserProfile')
})
})
describe('transactions', () => {
it('requires authorization', () => {
expect(routes.find((r) => r.path === '/transactions').meta.requiresAuth).toBeTruthy()
})
it('loads the "UserProfileTransactionList" component', async () => {
const component = await routes.find((r) => r.path === '/transactions').component()
expect(component.default.name).toBe('UserProfileTransactionList')
})
})
describe('login', () => {
it('loads the "Login" component', async () => {
const component = await routes.find((r) => r.path === '/login').component()
expect(component.default.name).toBe('login')
})
})
describe('thx', () => {
const thx = routes.find((r) => r.path === '/thx/:comingFrom')
it('loads the "Thx" component', async () => {
const component = await thx.component()
expect(component.default.name).toBe('Thx')
})
describe('beforeEnter', () => {
const beforeEnter = thx.beforeEnter
const next = jest.fn()
it('redirects to login when not coming from a valid page', () => {
beforeEnter({}, { path: '' }, next)
expect(next).toBeCalledWith({ path: '/login' })
})
it('enters the page when coming from a valid page', () => {
jest.resetAllMocks()
beforeEnter({}, { path: '/password' }, next)
expect(next).toBeCalledWith()
})
})
})
describe('password', () => {
it('loads the "Password" component', async () => {
const component = await routes.find((r) => r.path === '/password').component()
expect(component.default.name).toBe('password')
})
})
describe('reset', () => {
it('loads the "ResetPassword" component', async () => {
const component = await routes.find((r) => r.path === '/reset/:optin').component()
expect(component.default.name).toBe('ResetPassword')
})
})
describe('checkEmail', () => {
it('loads the "CheckEmail" component', async () => {
const component = await routes.find((r) => r.path === '/checkEmail/:optin').component()
expect(component.default.name).toBe('CheckEmail')
})
})
describe('not found page', () => {
it('renders the "NotFound" component', async () => {
expect(routes.find((r) => r.path === '*').component).toEqual(NotFound)
})
})
})
})
})

View File

@ -67,6 +67,7 @@ export const store = new Vuex.Store({
username: '',
description: '',
token: null,
coinanimation: true,
newsletterState: null,
},
getters: {},

View File

@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils'
import { GdtEntryType } from '../../../graphql/enums'
import GdtTransactionList from './GdtTransactionList'
const localVue = global.localVue
@ -14,7 +15,7 @@ const apolloMock = jest.fn().mockResolvedValue({
factor: 17,
comment: '',
date: '2021-05-02T17:20:11+00:00',
gdtEntryType: 1,
gdtEntryType: GdtEntryType.FORM,
},
{
amount: 1810,
@ -22,7 +23,7 @@ const apolloMock = jest.fn().mockResolvedValue({
factor: 0.2,
comment: 'Dezember 20',
date: '2020-12-31T12:00:00+00:00',
gdtEntryType: 7,
gdtEntryType: GdtEntryType.GLOBAL_MODIFICATOR,
},
{
amount: 100,
@ -30,7 +31,7 @@ const apolloMock = jest.fn().mockResolvedValue({
factor: 17,
comment: '',
date: '2020-05-07T17:00:00+00:00',
gdtEntryType: 1,
gdtEntryType: GdtEntryType.FORM,
},
{
amount: 100,
@ -38,7 +39,7 @@ const apolloMock = jest.fn().mockResolvedValue({
factor: 22,
comment: '',
date: '2020-04-10T13:28:00+00:00',
gdtEntryType: 4,
gdtEntryType: GdtEntryType.ELOPAGE_PUBLISHER,
},
],
},

View File

@ -0,0 +1,49 @@
import { mount } from '@vue/test-utils'
import UserCardCoinAnimation from './UserCard_CoinAnimation'
const localVue = global.localVue
const mockAPIcall = jest.fn()
const toastErrorMock = jest.fn()
const toastSuccessMock = jest.fn()
const storeCommitMock = jest.fn()
describe('UserCard_CoinAnimation', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$store: {
state: {
language: 'de',
},
commit: storeCommitMock,
},
$toasted: {
success: toastSuccessMock,
error: toastErrorMock,
},
$apollo: {
query: mockAPIcall,
},
}
const Wrapper = () => {
return mount(UserCardCoinAnimation, { localVue, mocks })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the component', () => {
expect(wrapper.find('div#formusercoinanimation').exists()).toBeTruthy()
})
it('has an edit BFormCheckbox switch', () => {
expect(wrapper.find('.Test-BFormCheckbox').exists()).toBeTruthy()
})
})
})

View File

@ -0,0 +1,68 @@
<template>
<b-card
id="formusercoinanimation"
class="bg-transparent"
style="background-color: #ebebeba3 !important; border-radius: 0px"
>
<div>
<b-row class="mb-3">
<b-col class="mb-2 col-12">
<small>
<b>{{ $t('settings.coinanimation.coinanimation') }}</b>
</small>
</b-col>
<b-col class="col-12">
<b-form-checkbox
class="Test-BFormCheckbox"
v-model="CoinAnimationStatus"
name="check-button"
switch
@change="onSubmit"
>
{{
CoinAnimationStatus
? $t('settings.coinanimation.True')
: $t('settings.coinanimation.False')
}}
</b-form-checkbox>
</b-col>
</b-row>
</div>
</b-card>
</template>
<script>
import { updateUserInfos } from '../../../graphql/mutations'
export default {
name: 'FormUserCoinAnimation',
data() {
return {
CoinAnimationStatus: true,
}
},
created() {
this.CoinAnimationStatus = this.$store.state.coinanimation /* existiert noch nicht im store */
},
methods: {
async onSubmit() {
this.$apollo
.mutate({
mutation: updateUserInfos,
variables: {
coinanimation: this.CoinAnimationStatus,
},
})
.then(() => {
this.$store.state.coinanimation = this.CoinAnimationStatus
this.$toasted.success(
this.CoinAnimationStatus
? this.$t('settings.coinanimation.True')
: this.$t('settings.coinanimation.False'),
)
})
.catch((error) => {
this.$toasted.error(error.message)
})
},
},
}
</script>

View File

@ -8,6 +8,8 @@
<form-user-language />
<hr />
<form-user-newsletter />
<hr />
<form-user-coin-animation />
</div>
</template>
<script>
@ -16,14 +18,17 @@ import FormUserData from './UserProfile/UserCard_FormUserData.vue'
import FormUserPasswort from './UserProfile/UserCard_FormUserPasswort.vue'
import FormUserLanguage from './UserProfile/UserCard_Language.vue'
import FormUserNewsletter from './UserProfile/UserCard_Newsletter.vue'
import FormUserCoinAnimation from './UserProfile/UserCard_CoinAnimation.vue'
export default {
name: 'UserProfile',
components: {
UserCard,
FormUserData,
FormUserPasswort,
FormUserLanguage,
FormUserNewsletter,
FormUserCoinAnimation,
},
props: {
balance: { type: Number, default: 0 },

View File

@ -35,6 +35,9 @@ module.exports = {
// 'process.env.PORT': JSON.stringify(process.env.PORT),
}),
],
infrastructureLogging: {
level: 'warn', // 'none' | 'error' | 'warn' | 'info' | 'log' | 'verbose'
},
},
css: {
// Enable CSS source maps.

View File

@ -15,7 +15,6 @@ Poco::JSON::Object* JsonUpdateUserInfos::handle(Poco::Dynamic::Var params)
{
/*
'session_id' => $session_id,
'email' => $email,
'update' => ['User.first_name' => 'first_name', 'User.last_name' => 'last_name', 'User.disabled' => 0|1, 'User.language' => 'de']
*/
// incoming
@ -34,8 +33,7 @@ Poco::JSON::Object* JsonUpdateUserInfos::handle(Poco::Dynamic::Var params)
/// not available for the given type.
/// Throws InvalidAccessException if Var is empty.
try {
paramJsonObject->get("email").convert(email);
auto session_id_obj = paramJsonObject->get("session_id");
if (!session_id_obj.isEmpty()) {
session_id_obj.convert(session_id);
@ -66,10 +64,7 @@ Poco::JSON::Object* JsonUpdateUserInfos::handle(Poco::Dynamic::Var params)
}
auto user = mSession->getNewUser();
auto user_model = user->getModel();
if (user_model->getEmail() != email) {
return customStateError("not same", "email don't belong to logged in user");
}
Poco::JSON::Object* result = new Poco::JSON::Object;
result->set("state", "success");
Poco::JSON::Array jsonErrorsArray;