mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into test-router
This commit is contained in:
commit
8e70007d0e
46
.github/workflows/test.yml
vendored
46
.github/workflows/test.yml
vendored
@ -328,7 +328,7 @@ jobs:
|
||||
docker run -v ~/coverage:/app/coverage --rm gradido/frontend:test yarn run test
|
||||
cp -r ~/coverage ./coverage
|
||||
##########################################################################
|
||||
# COVERAGE REPORT FRONTEND ################################################
|
||||
# COVERAGE REPORT FRONTEND ###############################################
|
||||
##########################################################################
|
||||
#- name: frontend | Coverage report
|
||||
# uses: romeovs/lcov-reporter-action@v0.2.21
|
||||
@ -347,6 +347,48 @@ jobs:
|
||||
min_coverage: 71
|
||||
token: ${{ github.token }}
|
||||
|
||||
##############################################################################
|
||||
# JOB: UNIT TEST BACKEND ####################################################
|
||||
##############################################################################
|
||||
unit_test_backend:
|
||||
name: Unit tests - Backend
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build_test_backend]
|
||||
steps:
|
||||
##########################################################################
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
##########################################################################
|
||||
# DOWNLOAD DOCKER IMAGES #################################################
|
||||
##########################################################################
|
||||
- name: Download Docker Image (Backend)
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: docker-backend-test
|
||||
path: /tmp
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/backend.tar
|
||||
##########################################################################
|
||||
# UNIT TESTS BACKEND #####################################################
|
||||
##########################################################################
|
||||
- name: backend | Unit tests
|
||||
run: |
|
||||
docker run -v ~/coverage:/app/coverage --rm gradido/backend:test yarn run test
|
||||
cp -r ~/coverage ./coverage
|
||||
##########################################################################
|
||||
# COVERAGE CHECK BACKEND #################################################
|
||||
##########################################################################
|
||||
- name: backend | Coverage check
|
||||
uses: webcraftmedia/coverage-check-action@master
|
||||
with:
|
||||
report_name: Coverage Backend
|
||||
type: lcov
|
||||
result_path: ./coverage/lcov.info
|
||||
min_coverage: 4
|
||||
token: ${{ github.token }}
|
||||
|
||||
##############################################################################
|
||||
# JOB: UNIT TEST LOGIN-SERVER ###############################################
|
||||
##############################################################################
|
||||
@ -428,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
|
||||
|
||||
3
backend/.gitignore
vendored
3
backend/.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
/node_modules/
|
||||
/.env
|
||||
/build/
|
||||
|
||||
package-json.lock
|
||||
coverage
|
||||
# emacs
|
||||
*~
|
||||
7
backend/jest.config.js
Normal file
7
backend/jest.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
verbose: true,
|
||||
preset: 'ts-jest',
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**'],
|
||||
}
|
||||
4024
backend/package-lock.json
generated
4024
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,9 +12,11 @@
|
||||
"clean": "tsc --build --clean",
|
||||
"start": "node build/index.js",
|
||||
"dev": "nodemon -w src --ext ts --exec ts-node src/index.ts",
|
||||
"lint": "eslint . --ext .js,.ts"
|
||||
"lint": "eslint . --ext .js,.ts",
|
||||
"test": "jest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/jest": "^27.0.2",
|
||||
"apollo-server-express": "^2.25.2",
|
||||
"axios": "^0.21.1",
|
||||
"class-validator": "^0.13.1",
|
||||
@ -22,9 +24,11 @@
|
||||
"dotenv": "^10.0.0",
|
||||
"express": "^4.17.1",
|
||||
"graphql": "^15.5.1",
|
||||
"jest": "^27.2.4",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mysql2": "^2.3.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"ts-jest": "^27.0.5",
|
||||
"type-graphql": "^1.1.1",
|
||||
"typeorm": "^0.2.37"
|
||||
},
|
||||
|
||||
@ -15,9 +15,10 @@ export const isAuthorized: AuthChecker<any> = async ({ root, args, context, info
|
||||
`${CONFIG.LOGIN_API_URL}checkSessionState?session_id=${decoded.sessionId}`,
|
||||
)
|
||||
context.sessionId = decoded.sessionId
|
||||
context.setHeaders.push({ key: 'token', value: encode(decoded.sessionId) })
|
||||
context.pubKey = decoded.pubKey
|
||||
context.setHeaders.push({ key: 'token', value: encode(decoded.sessionId, decoded.pubKey) })
|
||||
return result.success
|
||||
}
|
||||
}
|
||||
return false
|
||||
throw new Error('401 Unauthorized')
|
||||
}
|
||||
|
||||
13
backend/src/graphql/args/ChangePasswordArgs.ts
Normal file
13
backend/src/graphql/args/ChangePasswordArgs.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export default class ChangePasswordArgs {
|
||||
@Field(() => Number)
|
||||
sessionId: number
|
||||
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => String)
|
||||
password: string
|
||||
}
|
||||
10
backend/src/graphql/args/CheckUsernameArgs.ts
Normal file
10
backend/src/graphql/args/CheckUsernameArgs.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export default class CheckUsernameArgs {
|
||||
@Field(() => String)
|
||||
username: string
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
groupId?: number
|
||||
}
|
||||
19
backend/src/graphql/args/CreateUserArgs.ts
Normal file
19
backend/src/graphql/args/CreateUserArgs.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export default class CreateUserArgs {
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => String)
|
||||
firstName: string
|
||||
|
||||
@Field(() => String)
|
||||
lastName: string
|
||||
|
||||
@Field(() => String)
|
||||
password: string
|
||||
|
||||
@Field(() => String)
|
||||
language: string
|
||||
}
|
||||
14
backend/src/graphql/args/Paginated.ts
Normal file
14
backend/src/graphql/args/Paginated.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { ArgsType, Field, Int } from 'type-graphql'
|
||||
import { Order } from '../enum/Order'
|
||||
|
||||
@ArgsType()
|
||||
export default class Paginated {
|
||||
@Field(() => Int, { nullable: true })
|
||||
currentPage?: number
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
pageSize?: number
|
||||
|
||||
@Field(() => Order, { nullable: true })
|
||||
order?: Order
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export class SubscribeNewsletterArguments {
|
||||
export default class SubscribeNewsletterArgs {
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
13
backend/src/graphql/args/TransactionSendArgs.ts
Normal file
13
backend/src/graphql/args/TransactionSendArgs.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export default class TransactionSendArgs {
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => Number)
|
||||
amount: number
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
}
|
||||
10
backend/src/graphql/args/UnsecureLoginArgs.ts
Normal file
10
backend/src/graphql/args/UnsecureLoginArgs.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export default class UnsecureLoginArgs {
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => String)
|
||||
password: string
|
||||
}
|
||||
31
backend/src/graphql/args/UpdateUserInfosArgs.ts
Normal file
31
backend/src/graphql/args/UpdateUserInfosArgs.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export default class UpdateUserInfosArgs {
|
||||
@Field({ nullable: true })
|
||||
firstName?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
lastName?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
description?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
username?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
language?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
publisherId?: number
|
||||
|
||||
@Field({ nullable: true })
|
||||
password?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
passwordNew?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
coinanimation?: boolean
|
||||
}
|
||||
16
backend/src/graphql/enum/GdtEntryType.ts
Normal file
16
backend/src/graphql/enum/GdtEntryType.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
export enum GdtEntryType {
|
||||
FORM = 1,
|
||||
CVS = 2,
|
||||
ELOPAGE = 3,
|
||||
ELOPAGE_PUBLISHER = 4,
|
||||
DIGISTORE = 5,
|
||||
CVS2 = 6,
|
||||
GLOBAL_MODIFICATOR = 7,
|
||||
}
|
||||
|
||||
registerEnumType(GdtEntryType, {
|
||||
name: 'GdtEntryType', // this one is mandatory
|
||||
description: 'Gdt Entry Source Type', // this one is optional
|
||||
})
|
||||
11
backend/src/graphql/enum/Order.ts
Normal file
11
backend/src/graphql/enum/Order.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
export enum Order {
|
||||
ASC = 'ASC',
|
||||
DESC = 'DESC',
|
||||
}
|
||||
|
||||
registerEnumType(Order, {
|
||||
name: 'Order', // this one is mandatory
|
||||
description: 'Order direction - ascending or descending', // this one is optional
|
||||
})
|
||||
5
backend/src/graphql/enum/Setting.ts
Normal file
5
backend/src/graphql/enum/Setting.ts
Normal file
@ -0,0 +1,5 @@
|
||||
enum Setting {
|
||||
COIN_ANIMATION = 'coinanimation',
|
||||
}
|
||||
|
||||
export { Setting }
|
||||
12
backend/src/graphql/enum/TransactionType.ts
Normal file
12
backend/src/graphql/enum/TransactionType.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
export enum TransactionType {
|
||||
CREATION = 'creation',
|
||||
SEND = 'send',
|
||||
RECIEVE = 'receive',
|
||||
}
|
||||
|
||||
registerEnumType(TransactionType, {
|
||||
name: 'TransactionType', // this one is mandatory
|
||||
description: 'Name of the Type of the transaction', // this one is optional
|
||||
})
|
||||
11
backend/src/graphql/enum/TransactionTypeId.ts
Normal file
11
backend/src/graphql/enum/TransactionTypeId.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
export enum TransactionTypeId {
|
||||
CREATION = 1,
|
||||
SEND = 2,
|
||||
}
|
||||
|
||||
registerEnumType(TransactionTypeId, {
|
||||
name: 'TransactionTypeId', // this one is mandatory
|
||||
description: 'Type of the transaction', // this one is optional
|
||||
})
|
||||
@ -1,28 +0,0 @@
|
||||
import { ArgsType, Field, Int } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export class GdtTransactionInput {
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
currentPage?: number
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
pageSize?: number
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
order?: string
|
||||
}
|
||||
|
||||
@ArgsType()
|
||||
export class GdtTransactionSessionIdInput {
|
||||
@Field(() => Int, { nullable: true })
|
||||
currentPage?: number
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
pageSize?: number
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
order?: string
|
||||
}
|
||||
@ -1,79 +0,0 @@
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export class UnsecureLoginArgs {
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => String)
|
||||
password: string
|
||||
}
|
||||
|
||||
@ArgsType()
|
||||
export class CreateUserArgs {
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => String)
|
||||
firstName: string
|
||||
|
||||
@Field(() => String)
|
||||
lastName: string
|
||||
|
||||
@Field(() => String)
|
||||
password: string
|
||||
|
||||
@Field(() => String)
|
||||
language: string
|
||||
}
|
||||
|
||||
@ArgsType()
|
||||
export class ChangePasswordArgs {
|
||||
@Field(() => Number)
|
||||
sessionId: number
|
||||
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => String)
|
||||
password: string
|
||||
}
|
||||
|
||||
@ArgsType()
|
||||
export class UpdateUserInfosArgs {
|
||||
@Field(() => String)
|
||||
email!: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
firstName?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
lastName?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
description?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
username?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
language?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
publisherId?: number
|
||||
|
||||
@Field({ nullable: true })
|
||||
password?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
passwordNew?: string
|
||||
}
|
||||
|
||||
@ArgsType()
|
||||
export class CheckUsernameArgs {
|
||||
@Field(() => String)
|
||||
username: string
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
groupId?: number
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
import { ArgsType, Field, Int } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export class TransactionListInput {
|
||||
@Field(() => Int)
|
||||
firstPage: number
|
||||
|
||||
@Field(() => Int)
|
||||
items: number
|
||||
|
||||
@Field(() => String)
|
||||
order: string
|
||||
}
|
||||
|
||||
@ArgsType()
|
||||
export class TransactionSendArgs {
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => Number)
|
||||
amount: number
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
}
|
||||
@ -5,25 +5,29 @@ import { ObjectType, Field, Int } from 'type-graphql'
|
||||
@ObjectType()
|
||||
export class Decay {
|
||||
constructor(json: any) {
|
||||
this.balance = Number(json.balance)
|
||||
this.decayStart = json.decay_start
|
||||
this.decayEnd = json.decay_end
|
||||
this.decayDuration = json.decay_duration
|
||||
this.decayStartBlock = json.decay_start_block
|
||||
if (json) {
|
||||
this.balance = Number(json.balance)
|
||||
this.decayStart = json.decay_start
|
||||
this.decayEnd = json.decay_end
|
||||
this.decayDuration = json.decay_duration
|
||||
this.decayStartBlock = json.decay_start_block
|
||||
}
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
balance: number
|
||||
|
||||
// timestamp in seconds
|
||||
@Field(() => Int, { nullable: true })
|
||||
decayStart?: number
|
||||
decayStart: string
|
||||
|
||||
// timestamp in seconds
|
||||
@Field(() => Int, { nullable: true })
|
||||
decayEnd?: number
|
||||
decayEnd: string
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
decayDuration?: string
|
||||
decayDuration?: number
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
decayStartBlock?: number
|
||||
decayStartBlock?: string
|
||||
}
|
||||
|
||||
@ -1,16 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
|
||||
export enum GdtEntryType {
|
||||
FORM = 1,
|
||||
CVS = 2,
|
||||
ELOPAGE = 3,
|
||||
ELOPAGE_PUBLISHER = 4,
|
||||
DIGISTORE = 5,
|
||||
CVS2 = 6,
|
||||
GLOBAL_MODIFICATOR = 7,
|
||||
}
|
||||
import { GdtEntryType } from '../enum/GdtEntryType'
|
||||
|
||||
@ObjectType()
|
||||
export class GdtEntry {
|
||||
@ -46,7 +37,7 @@ export class GdtEntry {
|
||||
@Field(() => String)
|
||||
couponCode: string
|
||||
|
||||
@Field(() => Number)
|
||||
@Field(() => GdtEntryType)
|
||||
gdtEntryType: GdtEntryType
|
||||
|
||||
@Field(() => Number)
|
||||
|
||||
@ -3,20 +3,6 @@
|
||||
import { GdtEntry } from './GdtEntry'
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
|
||||
@ObjectType()
|
||||
export class GdtSumPerEmail {
|
||||
constructor(email: string, summe: number) {
|
||||
this.email = email
|
||||
this.summe = summe
|
||||
}
|
||||
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => Number)
|
||||
summe: number
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class GdtEntryList {
|
||||
constructor(json: any) {
|
||||
|
||||
19
backend/src/graphql/models/GdtSumPerEmail.ts
Normal file
19
backend/src/graphql/models/GdtSumPerEmail.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/*
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
|
||||
@ObjectType()
|
||||
export class GdtSumPerEmail {
|
||||
constructor(email: string, summe: number) {
|
||||
this.email = email
|
||||
this.summe = summe
|
||||
}
|
||||
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => Number)
|
||||
summe: number
|
||||
}
|
||||
*/
|
||||
@ -10,18 +10,11 @@ import { Decay } from './Decay'
|
||||
|
||||
@ObjectType()
|
||||
export class Transaction {
|
||||
constructor(json: any) {
|
||||
this.type = json.type
|
||||
this.balance = Number(json.balance)
|
||||
this.decayStart = json.decay_start
|
||||
this.decayEnd = json.decay_end
|
||||
this.decayDuration = json.decay_duration
|
||||
this.memo = json.memo
|
||||
this.transactionId = json.transaction_id
|
||||
this.name = json.name
|
||||
this.email = json.email
|
||||
this.date = json.date
|
||||
this.decay = json.decay ? new Decay(json.decay) : undefined
|
||||
constructor() {
|
||||
this.type = ''
|
||||
this.balance = 0
|
||||
this.totalBalance = 0
|
||||
this.memo = ''
|
||||
}
|
||||
|
||||
@Field(() => String)
|
||||
@ -30,14 +23,17 @@ export class Transaction {
|
||||
@Field(() => Number)
|
||||
balance: number
|
||||
|
||||
@Field({ nullable: true })
|
||||
decayStart?: number
|
||||
@Field(() => Number)
|
||||
totalBalance: number
|
||||
|
||||
@Field({ nullable: true })
|
||||
decayEnd?: number
|
||||
decayStart?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
decayDuration?: string
|
||||
decayEnd?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
decayDuration?: number
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
@ -57,35 +53,3 @@ export class Transaction {
|
||||
@Field({ nullable: true })
|
||||
decay?: Decay
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class TransactionList {
|
||||
constructor(json: any) {
|
||||
this.gdtSum = Number(json.gdtSum)
|
||||
this.count = json.count
|
||||
this.balance = Number(json.balance)
|
||||
this.decay = Number(json.decay)
|
||||
this.decayDate = json.decay_date
|
||||
this.transactions = json.transactions.map((el: any) => {
|
||||
return new Transaction(el)
|
||||
})
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
gdtSum: number
|
||||
|
||||
@Field(() => Number)
|
||||
count: number
|
||||
|
||||
@Field(() => Number)
|
||||
balance: number
|
||||
|
||||
@Field(() => Number)
|
||||
decay: number
|
||||
|
||||
@Field(() => String)
|
||||
decayDate: string
|
||||
|
||||
@Field(() => [Transaction])
|
||||
transactions: Transaction[]
|
||||
}
|
||||
|
||||
33
backend/src/graphql/models/TransactionList.ts
Normal file
33
backend/src/graphql/models/TransactionList.ts
Normal file
@ -0,0 +1,33 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
import { Transaction } from './Transaction'
|
||||
|
||||
@ObjectType()
|
||||
export class TransactionList {
|
||||
constructor() {
|
||||
this.gdtSum = 0
|
||||
this.count = 0
|
||||
this.balance = 0
|
||||
this.decay = 0
|
||||
this.decayDate = ''
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
gdtSum: number
|
||||
|
||||
@Field(() => Number)
|
||||
count: number
|
||||
|
||||
@Field(() => Number)
|
||||
balance: number
|
||||
|
||||
@Field(() => Number)
|
||||
decay: number
|
||||
|
||||
@Field(() => String)
|
||||
decayDate: string
|
||||
|
||||
@Field(() => [Transaction])
|
||||
transactions: Transaction[]
|
||||
}
|
||||
@ -69,6 +69,9 @@ export class User {
|
||||
@Field(() => Number)
|
||||
publisherId: number
|
||||
|
||||
@Field(() => Boolean)
|
||||
coinanimation: boolean
|
||||
|
||||
@Field(() => KlickTipp)
|
||||
klickTipp: KlickTipp
|
||||
}
|
||||
|
||||
@ -2,12 +2,11 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { Resolver, Query, Ctx, Authorized } from 'type-graphql'
|
||||
import CONFIG from '../../config'
|
||||
import { getCustomRepository } from 'typeorm'
|
||||
import { Balance } from '../models/Balance'
|
||||
import { apiGet } from '../../apis/HttpRequest'
|
||||
import { User as dbUser } from '../../typeorm/entity/User'
|
||||
import { Balance as dbBalance } from '../../typeorm/entity/Balance'
|
||||
import calculateDecay from '../../util/decay'
|
||||
import { BalanceRepository } from '../../typeorm/repository/Balance'
|
||||
import { UserRepository } from '../../typeorm/repository/User'
|
||||
import { calculateDecay } from '../../util/decay'
|
||||
import { roundFloorFrom4 } from '../../util/round'
|
||||
|
||||
@Resolver()
|
||||
@ -15,20 +14,29 @@ export class BalanceResolver {
|
||||
@Authorized()
|
||||
@Query(() => Balance)
|
||||
async balance(@Ctx() context: any): Promise<Balance> {
|
||||
// get public key for current logged in user
|
||||
const result = await apiGet(CONFIG.LOGIN_API_URL + 'login?session_id=' + context.sessionId)
|
||||
if (!result.success) throw new Error(result.data)
|
||||
|
||||
// load user and balance
|
||||
const userEntity = await dbUser.findByPubkeyHex(result.data.user.public_hex)
|
||||
const balanceEntity = await dbBalance.findByUser(userEntity.id)
|
||||
const balanceRepository = getCustomRepository(BalanceRepository)
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
|
||||
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
const balanceEntity = await balanceRepository.findByUser(userEntity.id)
|
||||
const now = new Date()
|
||||
const balance = new Balance({
|
||||
|
||||
// No balance found
|
||||
if (!balanceEntity) {
|
||||
return new Balance({
|
||||
balance: 0,
|
||||
decay: 0,
|
||||
decay_date: now.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
return new Balance({
|
||||
balance: roundFloorFrom4(balanceEntity.amount),
|
||||
decay: roundFloorFrom4(calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now)),
|
||||
decay: roundFloorFrom4(
|
||||
await calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now),
|
||||
),
|
||||
decay_date: now.toString(),
|
||||
})
|
||||
|
||||
return balance
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,11 +2,13 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { Resolver, Query, Args, Ctx, Authorized } from 'type-graphql'
|
||||
import { getCustomRepository } from 'typeorm'
|
||||
import CONFIG from '../../config'
|
||||
import { GdtEntryList } from '../models/GdtEntryList'
|
||||
import { GdtTransactionSessionIdInput } from '../inputs/GdtInputs'
|
||||
import Paginated from '../args/Paginated'
|
||||
import { apiGet } from '../../apis/HttpRequest'
|
||||
import { User as dbUser } from '../../typeorm/entity/User'
|
||||
import { UserRepository } from '../../typeorm/repository/User'
|
||||
import { Order } from '../enum/Order'
|
||||
|
||||
@Resolver()
|
||||
export class GdtResolver {
|
||||
@ -15,23 +17,19 @@ export class GdtResolver {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async listGDTEntries(
|
||||
@Args()
|
||||
{ currentPage = 1, pageSize = 5, order = 'DESC' }: GdtTransactionSessionIdInput,
|
||||
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
||||
@Ctx() context: any,
|
||||
): Promise<GdtEntryList> {
|
||||
// get public key for current logged in user
|
||||
const result = await apiGet(CONFIG.LOGIN_API_URL + 'login?session_id=' + context.sessionId)
|
||||
if (!result.success) throw new Error(result.data)
|
||||
|
||||
// load user
|
||||
const userEntity = await dbUser.findByPubkeyHex(result.data.user.public_hex)
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
|
||||
const resultGDT = await apiGet(
|
||||
`${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.email}/${currentPage}/${pageSize}/${order}`,
|
||||
)
|
||||
if (!resultGDT.success) {
|
||||
throw new Error(result.data)
|
||||
throw new Error(resultGDT.data)
|
||||
}
|
||||
|
||||
return new GdtEntryList(resultGDT.data)
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
unsubscribe,
|
||||
signIn,
|
||||
} from '../../apis/KlicktippController'
|
||||
import { SubscribeNewsletterArguments } from '../inputs/KlickTippInputs'
|
||||
import SubscribeNewsletterArgs from '../args/SubscribeNewsletterArgs'
|
||||
|
||||
@Resolver()
|
||||
export class KlicktippResolver {
|
||||
@ -33,7 +33,7 @@ export class KlicktippResolver {
|
||||
@Authorized()
|
||||
@Mutation(() => Boolean)
|
||||
async subscribeNewsletter(
|
||||
@Args() { email, language }: SubscribeNewsletterArguments,
|
||||
@Args() { email, language }: SubscribeNewsletterArgs,
|
||||
): Promise<boolean> {
|
||||
return await signIn(email, language)
|
||||
}
|
||||
|
||||
@ -2,24 +2,250 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
|
||||
import { getCustomRepository } from 'typeorm'
|
||||
|
||||
import CONFIG from '../../config'
|
||||
import { TransactionList } from '../models/Transaction'
|
||||
import { TransactionListInput, TransactionSendArgs } from '../inputs/TransactionInput'
|
||||
|
||||
import { Transaction } from '../models/Transaction'
|
||||
import { TransactionList } from '../models/TransactionList'
|
||||
|
||||
import TransactionSendArgs from '../args/TransactionSendArgs'
|
||||
import Paginated from '../args/Paginated'
|
||||
|
||||
import { Order } from '../enum/Order'
|
||||
|
||||
import { BalanceRepository } from '../../typeorm/repository/Balance'
|
||||
import { UserRepository } from '../../typeorm/repository/User'
|
||||
import { UserTransactionRepository } from '../../typeorm/repository/UserTransaction'
|
||||
import { TransactionRepository } from '../../typeorm/repository/Transaction'
|
||||
|
||||
import { User as dbUser } from '../../typeorm/entity/User'
|
||||
import { UserTransaction as dbUserTransaction } from '../../typeorm/entity/UserTransaction'
|
||||
import { Transaction as dbTransaction } from '../../typeorm/entity/Transaction'
|
||||
|
||||
import { apiGet, apiPost } from '../../apis/HttpRequest'
|
||||
import { roundFloorFrom4 } from '../../util/round'
|
||||
import { calculateDecay, calculateDecayWithInterval } from '../../util/decay'
|
||||
import { TransactionTypeId } from '../enum/TransactionTypeId'
|
||||
import { TransactionType } from '../enum/TransactionType'
|
||||
|
||||
// Helper function
|
||||
async function calculateAndAddDecayTransactions(
|
||||
userTransactions: dbUserTransaction[],
|
||||
user: dbUser,
|
||||
decay: boolean,
|
||||
skipFirstTransaction: boolean,
|
||||
): Promise<Transaction[]> {
|
||||
const finalTransactions: Transaction[] = []
|
||||
const transactionIds: number[] = []
|
||||
const involvedUserIds: number[] = []
|
||||
|
||||
userTransactions.forEach((userTransaction: dbUserTransaction) => {
|
||||
transactionIds.push(userTransaction.transactionId)
|
||||
})
|
||||
|
||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
||||
const transactions = await transactionRepository.joinFullTransactionsByIds(transactionIds)
|
||||
|
||||
const transactionIndiced: dbTransaction[] = []
|
||||
transactions.forEach((transaction: dbTransaction) => {
|
||||
transactionIndiced[transaction.id] = transaction
|
||||
if (transaction.transactionTypeId === TransactionTypeId.SEND) {
|
||||
involvedUserIds.push(transaction.transactionSendCoin.userId)
|
||||
involvedUserIds.push(transaction.transactionSendCoin.recipiantUserId)
|
||||
}
|
||||
})
|
||||
// remove duplicates
|
||||
// https://stackoverflow.com/questions/1960473/get-all-unique-values-in-a-javascript-array-remove-duplicates
|
||||
const involvedUsersUnique = involvedUserIds.filter((v, i, a) => a.indexOf(v) === i)
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const userIndiced = await userRepository.getUsersIndiced(involvedUsersUnique)
|
||||
|
||||
const decayStartTransaction = await transactionRepository.findDecayStartBlock()
|
||||
|
||||
for (let i = 0; i < userTransactions.length; i++) {
|
||||
const userTransaction = userTransactions[i]
|
||||
const transaction = transactionIndiced[userTransaction.transactionId]
|
||||
const finalTransaction = new Transaction()
|
||||
finalTransaction.transactionId = transaction.id
|
||||
finalTransaction.date = transaction.received.toISOString()
|
||||
finalTransaction.memo = transaction.memo
|
||||
finalTransaction.totalBalance = roundFloorFrom4(userTransaction.balance)
|
||||
const prev = i > 0 ? userTransactions[i - 1] : null
|
||||
|
||||
if (prev && prev.balance > 0) {
|
||||
const current = userTransaction
|
||||
const decay = await calculateDecayWithInterval(
|
||||
prev.balance,
|
||||
prev.balanceDate,
|
||||
current.balanceDate,
|
||||
)
|
||||
const balance = prev.balance - decay.balance
|
||||
|
||||
if (balance) {
|
||||
finalTransaction.decay = decay
|
||||
finalTransaction.decay.balance = roundFloorFrom4(balance)
|
||||
if (
|
||||
decayStartTransaction &&
|
||||
prev.transactionId < decayStartTransaction.id &&
|
||||
current.transactionId > decayStartTransaction.id
|
||||
) {
|
||||
finalTransaction.decay.decayStartBlock = (
|
||||
decayStartTransaction.received.getTime() / 1000
|
||||
).toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sender or receiver when user has sent money
|
||||
// group name if creation
|
||||
// type: gesendet / empfangen / geschöpft
|
||||
// transaktion nr / id
|
||||
// date
|
||||
// balance
|
||||
if (userTransaction.transactionTypeId === TransactionTypeId.CREATION) {
|
||||
// creation
|
||||
const creation = transaction.transactionCreation
|
||||
|
||||
finalTransaction.name = 'Gradido Akademie'
|
||||
finalTransaction.type = TransactionType.CREATION
|
||||
// finalTransaction.targetDate = creation.targetDate
|
||||
finalTransaction.balance = roundFloorFrom4(creation.amount)
|
||||
} else if (userTransaction.transactionTypeId === TransactionTypeId.SEND) {
|
||||
// send coin
|
||||
const sendCoin = transaction.transactionSendCoin
|
||||
let otherUser: dbUser | undefined
|
||||
finalTransaction.balance = roundFloorFrom4(sendCoin.amount)
|
||||
if (sendCoin.userId === user.id) {
|
||||
finalTransaction.type = TransactionType.SEND
|
||||
otherUser = userIndiced[sendCoin.recipiantUserId]
|
||||
// finalTransaction.pubkey = sendCoin.recipiantPublic
|
||||
} else if (sendCoin.recipiantUserId === user.id) {
|
||||
finalTransaction.type = TransactionType.RECIEVE
|
||||
otherUser = userIndiced[sendCoin.userId]
|
||||
// finalTransaction.pubkey = sendCoin.senderPublic
|
||||
} else {
|
||||
throw new Error('invalid transaction')
|
||||
}
|
||||
if (otherUser) {
|
||||
finalTransaction.name = otherUser.firstName + ' ' + otherUser.lastName
|
||||
finalTransaction.email = otherUser.email
|
||||
}
|
||||
}
|
||||
if (i > 0 || !skipFirstTransaction) {
|
||||
finalTransactions.push(finalTransaction)
|
||||
}
|
||||
|
||||
if (i === userTransactions.length - 1 && decay) {
|
||||
const now = new Date()
|
||||
const decay = await calculateDecayWithInterval(
|
||||
userTransaction.balance,
|
||||
userTransaction.balanceDate,
|
||||
now.getTime(),
|
||||
)
|
||||
const balance = userTransaction.balance - decay.balance
|
||||
if (balance) {
|
||||
const decayTransaction = new Transaction()
|
||||
decayTransaction.type = 'decay'
|
||||
decayTransaction.balance = roundFloorFrom4(balance)
|
||||
decayTransaction.decayDuration = decay.decayDuration
|
||||
decayTransaction.decayStart = decay.decayStart
|
||||
decayTransaction.decayEnd = decay.decayEnd
|
||||
finalTransactions.push(decayTransaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return finalTransactions
|
||||
}
|
||||
|
||||
// Helper function
|
||||
async function listTransactions(
|
||||
currentPage: number,
|
||||
pageSize: number,
|
||||
order: Order,
|
||||
user: dbUser,
|
||||
): Promise<TransactionList> {
|
||||
let limit = pageSize
|
||||
let offset = 0
|
||||
let skipFirstTransaction = false
|
||||
if (currentPage > 1) {
|
||||
offset = (currentPage - 1) * pageSize - 1
|
||||
limit++
|
||||
}
|
||||
|
||||
if (offset && order === Order.ASC) {
|
||||
offset--
|
||||
}
|
||||
const userTransactionRepository = getCustomRepository(UserTransactionRepository)
|
||||
let [userTransactions, userTransactionsCount] = await userTransactionRepository.findByUserPaged(
|
||||
user.id,
|
||||
limit,
|
||||
offset,
|
||||
order,
|
||||
)
|
||||
skipFirstTransaction = userTransactionsCount > offset + limit
|
||||
const decay = !(currentPage > 1)
|
||||
let transactions: Transaction[] = []
|
||||
if (userTransactions.length) {
|
||||
if (order === Order.DESC) {
|
||||
userTransactions = userTransactions.reverse()
|
||||
}
|
||||
transactions = await calculateAndAddDecayTransactions(
|
||||
userTransactions,
|
||||
user,
|
||||
decay,
|
||||
skipFirstTransaction,
|
||||
)
|
||||
if (order === Order.DESC) {
|
||||
transactions = transactions.reverse()
|
||||
}
|
||||
}
|
||||
|
||||
const transactionList = new TransactionList()
|
||||
transactionList.count = userTransactionsCount
|
||||
transactionList.transactions = transactions
|
||||
return transactionList
|
||||
}
|
||||
|
||||
@Resolver()
|
||||
export class TransactionResolver {
|
||||
@Authorized()
|
||||
@Query(() => TransactionList)
|
||||
async transactionList(
|
||||
@Args() { firstPage = 1, items = 25, order = 'DESC' }: TransactionListInput,
|
||||
@Args() { currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
|
||||
@Ctx() context: any,
|
||||
): Promise<TransactionList> {
|
||||
const result = await apiGet(
|
||||
`${CONFIG.COMMUNITY_API_URL}listTransactions/${firstPage}/${items}/${order}/${context.sessionId}`,
|
||||
)
|
||||
// get public key for current logged in user
|
||||
const result = await apiGet(CONFIG.LOGIN_API_URL + 'login?session_id=' + context.sessionId)
|
||||
if (!result.success) throw new Error(result.data)
|
||||
return new TransactionList(result.data)
|
||||
|
||||
// load user
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const userEntity = await userRepository.findByPubkeyHex(result.data.user.public_hex)
|
||||
|
||||
const transactions = await listTransactions(currentPage, pageSize, order, userEntity)
|
||||
|
||||
// get gdt sum
|
||||
const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, {
|
||||
email: userEntity.email,
|
||||
})
|
||||
if (!resultGDTSum.success) throw new Error(resultGDTSum.data)
|
||||
transactions.gdtSum = resultGDTSum.data.sum || 0
|
||||
|
||||
// get balance
|
||||
const balanceRepository = getCustomRepository(BalanceRepository)
|
||||
const balanceEntity = await balanceRepository.findByUser(userEntity.id)
|
||||
if (balanceEntity) {
|
||||
const now = new Date()
|
||||
transactions.balance = roundFloorFrom4(balanceEntity.amount)
|
||||
transactions.decay = roundFloorFrom4(
|
||||
await calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now),
|
||||
)
|
||||
transactions.decayDate = now.toString()
|
||||
}
|
||||
|
||||
return transactions
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
|
||||
@ -9,19 +9,22 @@ import { SendPasswordResetEmailResponse } from '../models/SendPasswordResetEmail
|
||||
import { UpdateUserInfosResponse } from '../models/UpdateUserInfosResponse'
|
||||
import { User } from '../models/User'
|
||||
import encode from '../../jwt/encode'
|
||||
import {
|
||||
ChangePasswordArgs,
|
||||
CheckUsernameArgs,
|
||||
CreateUserArgs,
|
||||
UnsecureLoginArgs,
|
||||
UpdateUserInfosArgs,
|
||||
} from '../inputs/LoginUserInput'
|
||||
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 { apiPost, apiGet } from '../../apis/HttpRequest'
|
||||
import {
|
||||
klicktippRegistrationMiddleware,
|
||||
klicktippNewsletterStateMiddleware,
|
||||
} from '../../middleware/klicktippMiddleware'
|
||||
import { CheckEmailResponse } from '../models/CheckEmailResponse'
|
||||
import { getCustomRepository } from 'typeorm'
|
||||
import { UserSettingRepository } from '../../typeorm/repository/UserSettingRepository'
|
||||
import { Setting } from '../../graphql/enum/Setting'
|
||||
import { UserRepository } from '../../typeorm/repository/User'
|
||||
|
||||
@Resolver()
|
||||
export class UserResolver {
|
||||
@Query(() => User)
|
||||
@ -35,9 +38,23 @@ export class UserResolver {
|
||||
throw new Error(result.data)
|
||||
}
|
||||
|
||||
context.setHeaders.push({ key: 'token', value: encode(result.data.session_id) })
|
||||
context.setHeaders.push({
|
||||
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)
|
||||
@ -126,7 +143,6 @@ export class UserResolver {
|
||||
async updateUserInfos(
|
||||
@Args()
|
||||
{
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
description,
|
||||
@ -135,12 +151,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,
|
||||
@ -152,9 +168,42 @@ export class UserResolver {
|
||||
'User.password_old': password || undefined,
|
||||
},
|
||||
}
|
||||
const result = await apiPost(CONFIG.LOGIN_API_URL + 'updateUserInfos', payload)
|
||||
if (!result.success) throw new Error(result.data)
|
||||
return new UpdateUserInfosResponse(result.data)
|
||||
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)
|
||||
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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,18 +1,29 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import jwt from 'jsonwebtoken'
|
||||
import jwt, { JwtPayload } from 'jsonwebtoken'
|
||||
import CONFIG from '../config/'
|
||||
|
||||
export default (token: string): any => {
|
||||
if (!token) return new Error('401 Unauthorized')
|
||||
interface CustomJwtPayload extends JwtPayload {
|
||||
sessionId: number
|
||||
pubKey: Buffer
|
||||
}
|
||||
|
||||
type DecodedJwt = {
|
||||
token: string
|
||||
sessionId: number
|
||||
pubKey: Buffer
|
||||
}
|
||||
|
||||
export default (token: string): DecodedJwt => {
|
||||
if (!token) throw new Error('401 Unauthorized')
|
||||
let sessionId = null
|
||||
let pubKey = null
|
||||
try {
|
||||
const decoded = jwt.verify(token, CONFIG.JWT_SECRET)
|
||||
sessionId = decoded.sub
|
||||
const decoded = <CustomJwtPayload>jwt.verify(token, CONFIG.JWT_SECRET)
|
||||
sessionId = decoded.sessionId
|
||||
pubKey = decoded.pubKey
|
||||
return {
|
||||
token,
|
||||
sessionId,
|
||||
pubKey,
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error('403.13 - Client certificate revoked')
|
||||
|
||||
@ -5,8 +5,8 @@ import jwt from 'jsonwebtoken'
|
||||
import CONFIG from '../config/'
|
||||
|
||||
// Generate an Access Token
|
||||
export default function encode(sessionId: string): string {
|
||||
const token = jwt.sign({ sessionId }, CONFIG.JWT_SECRET, {
|
||||
export default function encode(sessionId: number, pubKey: Buffer): string {
|
||||
const token = jwt.sign({ sessionId, pubKey }, CONFIG.JWT_SECRET, {
|
||||
expiresIn: CONFIG.JWT_EXPIRES_IN,
|
||||
subject: sessionId.toString(),
|
||||
})
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
const context = (args: any) => {
|
||||
const authorization = args.req.headers.authorization
|
||||
let token = null
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
const plugins = [
|
||||
{
|
||||
requestDidStart() {
|
||||
|
||||
@ -16,10 +16,4 @@ export class Balance extends BaseEntity {
|
||||
|
||||
@Column({ type: 'bigint' })
|
||||
amount: number
|
||||
|
||||
static findByUser(userId: number): Promise<Balance> {
|
||||
return this.createQueryBuilder('balance')
|
||||
.where('balance.userId = :userId', { userId })
|
||||
.getOneOrFail()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
/* import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm";
|
||||
import { User } from "./User"
|
||||
|
||||
@Entity()
|
||||
export class Group {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
alias: string;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column()
|
||||
url: string;
|
||||
|
||||
@Column()
|
||||
description: string;
|
||||
|
||||
@OneToMany(type => User, user => user.group)
|
||||
users: User[];
|
||||
|
||||
} */
|
||||
30
backend/src/typeorm/entity/Transaction.ts
Normal file
30
backend/src/typeorm/entity/Transaction.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm'
|
||||
import { TransactionCreation } from './TransactionCreation'
|
||||
import { TransactionSendCoin } from './TransactionSendCoin'
|
||||
|
||||
@Entity('transactions')
|
||||
export class Transaction extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column({ name: 'transaction_type_id' })
|
||||
transactionTypeId: number
|
||||
|
||||
@Column({ name: 'tx_hash', type: 'binary', length: 48 })
|
||||
txHash: Buffer
|
||||
|
||||
@Column()
|
||||
memo: string
|
||||
|
||||
@Column({ type: 'timestamp' })
|
||||
received: Date
|
||||
|
||||
@Column({ name: 'blockchain_type_id' })
|
||||
blockchainTypeId: number
|
||||
|
||||
@OneToOne(() => TransactionSendCoin, (transactionSendCoin) => transactionSendCoin.transaction)
|
||||
transactionSendCoin: TransactionSendCoin
|
||||
|
||||
@OneToOne(() => TransactionCreation, (transactionCreation) => transactionCreation.transaction)
|
||||
transactionCreation: TransactionCreation
|
||||
}
|
||||
32
backend/src/typeorm/entity/TransactionCreation.ts
Normal file
32
backend/src/typeorm/entity/TransactionCreation.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
Timestamp,
|
||||
OneToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm'
|
||||
import { Transaction } from './Transaction'
|
||||
|
||||
@Entity('transaction_creations')
|
||||
export class TransactionCreation extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column({ name: 'transaction_id' })
|
||||
transactionId: number
|
||||
|
||||
@Column({ name: 'state_user_id' })
|
||||
userId: number
|
||||
|
||||
@Column()
|
||||
amount: number
|
||||
|
||||
@Column({ name: 'target_date', type: 'timestamp' })
|
||||
targetDate: Timestamp
|
||||
|
||||
@OneToOne(() => Transaction)
|
||||
@JoinColumn({ name: 'transaction_id' })
|
||||
transaction: Transaction
|
||||
}
|
||||
30
backend/src/typeorm/entity/TransactionSendCoin.ts
Normal file
30
backend/src/typeorm/entity/TransactionSendCoin.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm'
|
||||
import { Transaction } from './Transaction'
|
||||
|
||||
@Entity('transaction_send_coins')
|
||||
export class TransactionSendCoin extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column({ name: 'transaction_id' })
|
||||
transactionId: number
|
||||
|
||||
@Column({ name: 'sender_public_key', type: 'binary', length: 32 })
|
||||
senderPublic: Buffer
|
||||
|
||||
@Column({ name: 'state_user_id' })
|
||||
userId: number
|
||||
|
||||
@Column({ name: 'receiver_public_key', type: 'binary', length: 32 })
|
||||
recipiantPublic: Buffer
|
||||
|
||||
@Column({ name: 'receiver_user_id' })
|
||||
recipiantUserId: number
|
||||
|
||||
@Column()
|
||||
amount: number
|
||||
|
||||
@OneToOne(() => Transaction)
|
||||
@JoinColumn({ name: 'transaction_id' })
|
||||
transaction: Transaction
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'
|
||||
import { UserSetting } from './UserSetting'
|
||||
|
||||
// import { Group } from "./Group"
|
||||
// Moriz: I do not like the idea of having two user tables
|
||||
@Entity('state_users')
|
||||
export class User extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
@ -27,9 +28,6 @@ export class User extends BaseEntity {
|
||||
@Column()
|
||||
disabled: boolean
|
||||
|
||||
static findByPubkeyHex(pubkeyHex: string): Promise<User> {
|
||||
return this.createQueryBuilder('user')
|
||||
.where('hex(user.pubkey) = :pubkeyHex', { pubkeyHex })
|
||||
.getOneOrFail()
|
||||
}
|
||||
@OneToMany(() => UserSetting, (userSetting) => userSetting.user)
|
||||
settings: UserSetting[]
|
||||
}
|
||||
|
||||
20
backend/src/typeorm/entity/UserSetting.ts
Normal file
20
backend/src/typeorm/entity/UserSetting.ts
Normal 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
|
||||
}
|
||||
22
backend/src/typeorm/entity/UserTransaction.ts
Normal file
22
backend/src/typeorm/entity/UserTransaction.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
|
||||
|
||||
@Entity('state_user_transactions')
|
||||
export class UserTransaction extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column({ name: 'state_user_id' })
|
||||
userId: number
|
||||
|
||||
@Column({ name: 'transaction_id' })
|
||||
transactionId: number
|
||||
|
||||
@Column({ name: 'transaction_type_id' })
|
||||
transactionTypeId: number
|
||||
|
||||
@Column({ name: 'balance', type: 'bigint' })
|
||||
balance: number
|
||||
|
||||
@Column({ name: 'balance_date', type: 'timestamp' })
|
||||
balanceDate: Date
|
||||
}
|
||||
@ -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
|
||||
|
||||
9
backend/src/typeorm/repository/Balance.ts
Normal file
9
backend/src/typeorm/repository/Balance.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { EntityRepository, Repository } from 'typeorm'
|
||||
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 }).getOne()
|
||||
}
|
||||
}
|
||||
28
backend/src/typeorm/repository/Transaction.ts
Normal file
28
backend/src/typeorm/repository/Transaction.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { EntityRepository, Repository } from 'typeorm'
|
||||
import { Transaction } from '../entity/Transaction'
|
||||
|
||||
@EntityRepository(Transaction)
|
||||
export class TransactionRepository extends Repository<Transaction> {
|
||||
async findDecayStartBlock(): Promise<Transaction | undefined> {
|
||||
return this.createQueryBuilder('transaction')
|
||||
.where('transaction.transactionTypeId = :transactionTypeId', { transactionTypeId: 9 })
|
||||
.orderBy('received', 'ASC')
|
||||
.getOne()
|
||||
}
|
||||
|
||||
async joinFullTransactionsByIds(transactionIds: number[]): Promise<Transaction[]> {
|
||||
return this.createQueryBuilder('transaction')
|
||||
.where('transaction.id IN (:...transactions)', { transactions: transactionIds })
|
||||
.leftJoinAndSelect(
|
||||
'transaction.transactionSendCoin',
|
||||
'transactionSendCoin',
|
||||
// 'transactionSendCoin.transaction_id = transaction.id',
|
||||
)
|
||||
.leftJoinAndSelect(
|
||||
'transaction.transactionCreation',
|
||||
'transactionCreation',
|
||||
// 'transactionSendCoin.transaction_id = transaction.id',
|
||||
)
|
||||
.getMany()
|
||||
}
|
||||
}
|
||||
23
backend/src/typeorm/repository/User.ts
Normal file
23
backend/src/typeorm/repository/User.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { EntityRepository, Repository } from 'typeorm'
|
||||
import { User } from '../entity/User'
|
||||
|
||||
@EntityRepository(User)
|
||||
export class UserRepository extends Repository<User> {
|
||||
async findByPubkeyHex(pubkeyHex: string): Promise<User> {
|
||||
return this.createQueryBuilder('user')
|
||||
.where('hex(user.pubkey) = :pubkeyHex', { pubkeyHex })
|
||||
.getOneOrFail()
|
||||
}
|
||||
|
||||
async getUsersIndiced(userIds: number[]): Promise<User[]> {
|
||||
const users = await this.createQueryBuilder('user')
|
||||
.select(['user.id', 'user.firstName', 'user.lastName', 'user.email'])
|
||||
.where('user.id IN (:...users)', { users: userIds })
|
||||
.getMany()
|
||||
const usersIndiced: User[] = []
|
||||
users.forEach((value) => {
|
||||
usersIndiced[value.id] = value
|
||||
})
|
||||
return usersIndiced
|
||||
}
|
||||
}
|
||||
36
backend/src/typeorm/repository/UserSettingRepository.ts
Normal file
36
backend/src/typeorm/repository/UserSettingRepository.ts
Normal 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'
|
||||
}
|
||||
}
|
||||
20
backend/src/typeorm/repository/UserTransaction.ts
Normal file
20
backend/src/typeorm/repository/UserTransaction.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { EntityRepository, Repository } from 'typeorm'
|
||||
import { Order } from '../../graphql/enum/Order'
|
||||
import { UserTransaction } from '../entity/UserTransaction'
|
||||
|
||||
@EntityRepository(UserTransaction)
|
||||
export class UserTransactionRepository extends Repository<UserTransaction> {
|
||||
findByUserPaged(
|
||||
userId: number,
|
||||
limit: number,
|
||||
offset: number,
|
||||
order: Order,
|
||||
): Promise<[UserTransaction[], number]> {
|
||||
return this.createQueryBuilder('userTransaction')
|
||||
.where('userTransaction.userId = :userId', { userId })
|
||||
.orderBy('userTransaction.balanceDate', order)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.getManyAndCount()
|
||||
}
|
||||
}
|
||||
16
backend/src/util/decay.test.ts
Normal file
16
backend/src/util/decay.test.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { calculateDecay } from './decay'
|
||||
|
||||
describe('utils/decay', () => {
|
||||
it.skip('has base 0.99999997802044727', async () => {
|
||||
const now = new Date()
|
||||
now.setSeconds(1)
|
||||
const oneSecondAgo = new Date(now.getTime())
|
||||
oneSecondAgo.setSeconds(0)
|
||||
expect(await calculateDecay(1.0, oneSecondAgo, now)).toBe(0.99999997802044727)
|
||||
})
|
||||
|
||||
it.skip('returns input amount when from and to is the same', async () => {
|
||||
const now = new Date()
|
||||
expect(await calculateDecay(100.0, now, now)).toBe(100.0)
|
||||
})
|
||||
})
|
||||
@ -1,4 +1,59 @@
|
||||
export default function (amount: number, from: Date, to: Date): number {
|
||||
const decayDuration = (to.getTime() - from.getTime()) / 1000
|
||||
return amount * Math.pow(0.99999997802044727, decayDuration)
|
||||
import { getCustomRepository } from 'typeorm'
|
||||
import { Decay } from '../graphql/models/Decay'
|
||||
import { TransactionRepository } from '../typeorm/repository/Transaction'
|
||||
|
||||
function decayFormula(amount: number, durationInSeconds: number): number {
|
||||
return amount * Math.pow(0.99999997802044727, durationInSeconds)
|
||||
}
|
||||
|
||||
async function calculateDecay(amount: number, from: Date, to: Date): Promise<number> {
|
||||
// load decay start block
|
||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
||||
const decayStartBlock = await transactionRepository.findDecayStartBlock()
|
||||
|
||||
// if decay hasn't started yet we return input amount
|
||||
if (!decayStartBlock) return amount
|
||||
|
||||
const decayDuration = (to.getTime() - from.getTime()) / 1000
|
||||
return decayFormula(amount, decayDuration)
|
||||
}
|
||||
|
||||
async function calculateDecayWithInterval(
|
||||
amount: number,
|
||||
from: number | Date,
|
||||
to: number | Date,
|
||||
): Promise<Decay> {
|
||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
||||
const decayStartBlock = await transactionRepository.findDecayStartBlock()
|
||||
|
||||
const result = new Decay(undefined)
|
||||
result.balance = amount
|
||||
const fromMillis = typeof from === 'number' ? from : from.getTime()
|
||||
const toMillis = typeof to === 'number' ? to : to.getTime()
|
||||
result.decayStart = (fromMillis / 1000).toString()
|
||||
result.decayEnd = (toMillis / 1000).toString()
|
||||
|
||||
// (amount, from.getTime(), to.getTime())
|
||||
|
||||
// if no decay start block exist or decay startet after end date
|
||||
if (!decayStartBlock || decayStartBlock.received.getTime() > toMillis) {
|
||||
return result
|
||||
}
|
||||
const decayStartBlockMillis = decayStartBlock.received.getTime()
|
||||
|
||||
// if decay start date is before start date we calculate decay for full duration
|
||||
if (decayStartBlockMillis < fromMillis) {
|
||||
result.decayDuration = toMillis - fromMillis
|
||||
}
|
||||
// if decay start in between start date and end date we caculcate decay from decay start time to end date
|
||||
else {
|
||||
result.decayDuration = toMillis - decayStartBlockMillis
|
||||
result.decayStart = (decayStartBlockMillis / 1000).toString()
|
||||
}
|
||||
// js use timestamp in milliseconds but we calculate with seconds
|
||||
result.decayDuration /= 1000
|
||||
result.balance = decayFormula(amount, result.decayDuration)
|
||||
return result
|
||||
}
|
||||
|
||||
export { calculateDecay, calculateDecayWithInterval }
|
||||
|
||||
9
backend/src/util/validate.ts
Normal file
9
backend/src/util/validate.ts
Normal 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 }
|
||||
3136
backend/yarn.lock
3136
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
26
database/migrations/0002-add_settings.ts
Normal file
26
database/migrations/0002-add_settings.ts
Normal 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\`;`)
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
103
docu/graphics/AnachBüberweisen.drawio
Normal file
103
docu/graphics/AnachBüberweisen.drawio
Normal file
@ -0,0 +1,103 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="ZvjvITeOyjQP4YKfGS0G" name="Page-1">
|
||||
<mxGraphModel dx="923" dy="562" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="29" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#008a00;strokeColor=#005700;fontColor=#ffffff;" vertex="1" parent="1">
|
||||
<mxGeometry x="660" y="135" width="120" height="215" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="28" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#60a917;strokeColor=#2D7600;fontColor=#ffffff;" vertex="1" parent="1">
|
||||
<mxGeometry x="540" y="135" width="120" height="215" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="27" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e3c800;strokeColor=#B09500;fontColor=#000000;" vertex="1" parent="1">
|
||||
<mxGeometry x="420" y="135" width="120" height="215" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="26" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f0a30a;strokeColor=#BD7000;fontColor=#000000;" vertex="1" parent="1">
|
||||
<mxGeometry x="300" y="135" width="120" height="215" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="25" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fa6800;strokeColor=#C73500;fontColor=#000000;" vertex="1" parent="1">
|
||||
<mxGeometry x="180" y="135" width="120" height="215" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="24" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e51400;strokeColor=#B20000;fontColor=#ffffff;" vertex="1" parent="1">
|
||||
<mxGeometry x="70" y="135" width="110" height="215" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;fillColor=#008a00;strokeColor=#005700;jumpSize=6;endSize=6;startSize=6;strokeWidth=3;" edge="1" parent="1" source="2" target="3">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fillColor=#008a00;strokeColor=#005700;strokeWidth=3;" edge="1" parent="1" source="2" target="4">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="2" value="1.5.0<br>Register" style="whiteSpace=wrap;html=1;aspect=fixed;fillColor=#2A2A2A;strokeColor=#F0F0F0;fontColor=#F0F0F0;" vertex="1" parent="1">
|
||||
<mxGeometry x="80" y="200" width="80" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="11" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="3" target="7">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="12" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;strokeWidth=3;fillColor=#60a917;strokeColor=#2D7600;" edge="1" parent="1" source="3" target="10">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="1.6.0<br>Kubernetes" style="whiteSpace=wrap;html=1;aspect=fixed;fillColor=#2A2A2A;strokeColor=#F0F0F0;fontColor=#F0F0F0;" vertex="1" parent="1">
|
||||
<mxGeometry x="200" y="150" width="80" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="13" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;strokeWidth=3;fillColor=#60a917;strokeColor=#2D7600;" edge="1" parent="1" source="4" target="7">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="14" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;strokeWidth=3;fillColor=#60a917;strokeColor=#2D7600;" edge="1" parent="1" source="4" target="10">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="1.6.0<br>Overview<br>Page" style="whiteSpace=wrap;html=1;aspect=fixed;fillColor=#2A2A2A;strokeColor=#F0F0F0;fontColor=#F0F0F0;" vertex="1" parent="1">
|
||||
<mxGeometry x="200" y="250" width="80" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="16" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;strokeWidth=3;fillColor=#e3c800;strokeColor=#B09500;" edge="1" parent="1" source="7" target="15">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="1.7.0<br>Federation" style="whiteSpace=wrap;html=1;aspect=fixed;fillColor=#2A2A2A;strokeColor=#F0F0F0;fontColor=#F0F0F0;" vertex="1" parent="1">
|
||||
<mxGeometry x="320" y="150" width="80" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;strokeWidth=3;fillColor=#e3c800;strokeColor=#B09500;" edge="1" parent="1" source="10" target="15">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="10" value="1.7.0<br>Statistics(?)" style="whiteSpace=wrap;html=1;aspect=fixed;fillColor=#2A2A2A;strokeColor=#F0F0F0;fontColor=#F0F0F0;" vertex="1" parent="1">
|
||||
<mxGeometry x="320" y="250" width="80" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="20" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;fillColor=#f0a30a;strokeColor=#BD7000;strokeWidth=3;" edge="1" parent="1" source="15" target="18">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="15" value="1.8.0<br>Activities" style="whiteSpace=wrap;html=1;aspect=fixed;fillColor=#2A2A2A;strokeColor=#F0F0F0;fontColor=#F0F0F0;" vertex="1" parent="1">
|
||||
<mxGeometry x="440" y="200" width="80" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="21" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;fillColor=#fa6800;strokeColor=#C73500;strokeWidth=3;" edge="1" parent="1" source="18" target="19">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="18" value="1.X.0<br>???" style="whiteSpace=wrap;html=1;aspect=fixed;fillColor=#2A2A2A;strokeColor=#F0F0F0;fontColor=#F0F0F0;" vertex="1" parent="1">
|
||||
<mxGeometry x="560" y="200" width="80" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="19" value="Von A nach B überweisen&nbsp;" style="whiteSpace=wrap;html=1;aspect=fixed;fillColor=#2A2A2A;strokeColor=#F0F0F0;fontColor=#F0F0F0;" vertex="1" parent="1">
|
||||
<mxGeometry x="680" y="200" width="80" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="30" value="The background color symbolizes how far away we are from the goal. The arrow color symbolizes how sure we can be that this will be the course of action" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontColor=#F0F0F0;" vertex="1" parent="1">
|
||||
<mxGeometry x="70" y="640" width="330" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="31" value="Our current release" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=top;whiteSpace=wrap;rounded=0;fontColor=#F0F0F0;" vertex="1" parent="1">
|
||||
<mxGeometry x="70" y="360" width="110" height="240" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="32" value="The Release 1.6.0 will make sure we can actually deploy multiple instances of our Software.<br>Additionally we will create the Overview Page in the Frontend" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=top;whiteSpace=wrap;rounded=0;fontColor=#F0F0F0;" vertex="1" parent="1">
|
||||
<mxGeometry x="185" y="360" width="110" height="240" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="33" value="The Release 1.7.0 will focus on the Federation. Maybe we will be able to send Coins at this stage, but its likely this will be done in the next steps. Here we most likely get to know the other communties only.<br>In the Frontend we consider to show some statisticts with the amount of known Communities" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=top;whiteSpace=wrap;rounded=0;fontColor=#F0F0F0;" vertex="1" parent="1">
|
||||
<mxGeometry x="305" y="361" width="110" height="239" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="35" value="The Release 1.8.0 will implement Activities in order to allow other communities to actually create GDD." style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=top;whiteSpace=wrap;rounded=0;fontColor=#F0F0F0;" vertex="1" parent="1">
|
||||
<mxGeometry x="425" y="360" width="110" height="240" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="36" value="More Steps might be needed to have the usable full functionallity of sending from A to B." style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=top;whiteSpace=wrap;rounded=0;fontColor=#F0F0F0;" vertex="1" parent="1">
|
||||
<mxGeometry x="545" y="360" width="110" height="240" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="37" value="Be aware that we might decide to split some Releases into two. Especially the Federation Release might need a followup Release in oder to actually send coins, since this will not be part of the Federation itself." style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontColor=#F0F0F0;" vertex="1" parent="1">
|
||||
<mxGeometry x="70" y="710" width="330" height="50" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
BIN
docu/graphics/AnachBüberweisen.png
Normal file
BIN
docu/graphics/AnachBüberweisen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
208
docu/graphics/gdt_server_api.drawio
Normal file
208
docu/graphics/gdt_server_api.drawio
Normal file
@ -0,0 +1,208 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="1NBLOcaJ18vLSwe3OBlU" name="Page-1">
|
||||
<mxGraphModel dx="1337" dy="381" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" background="#F1FAEE" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="78" value="External Service" style="rounded=0;whiteSpace=wrap;html=1;align=left;verticalAlign=top;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="600" y="1400" width="240" height="160" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="60" value="Community Gradido Akademie" style="rounded=0;whiteSpace=wrap;html=1;align=left;verticalAlign=top;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="279" y="840" width="240" height="320" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="57" value="Community B" style="rounded=0;whiteSpace=wrap;html=1;align=left;verticalAlign=top;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="600" y="840" width="240" height="320" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="Community Gradido Akademie" style="rounded=0;whiteSpace=wrap;html=1;align=left;verticalAlign=top;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="280" y="40" width="240" height="320" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="Current Process" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=21;fontStyle=1;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry y="10" width="180" height="20" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="9" value="EMail" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;startArrow=classic;startFill=1;strokeWidth=3;labelBackgroundColor=none;strokeColor=#2D7600;fontColor=#1D3557;fillColor=#60a917;" parent="1" source="6" target="7" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="6" value="Backend" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="320" y="80" width="160" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="GDT-Server" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="320" y="240" width="160" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="10" value="Community B" style="rounded=0;whiteSpace=wrap;html=1;align=left;verticalAlign=top;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="600" y="40" width="240" height="320" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="14" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=none;endFill=0;strokeWidth=3;labelBackgroundColor=#F1FAEE;strokeColor=#B20000;fontColor=#1D3557;fillColor=#e51400;" parent="1" source="12" target="7" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="12" value="Backend" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="640" y="80" width="160" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="16" value="" style="endArrow=none;html=1;strokeWidth=3;labelBackgroundColor=#F1FAEE;strokeColor=#B20000;fontColor=#1D3557;fillColor=#e51400;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="550" y="199" as="sourcePoint"/>
|
||||
<mxPoint x="570" y="184" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="17" value="" style="endArrow=none;html=1;strokeWidth=3;labelBackgroundColor=#F1FAEE;strokeColor=#B20000;fontColor=#1D3557;fillColor=#e51400;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="550" y="209.5" as="sourcePoint"/>
|
||||
<mxPoint x="570" y="194.5" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="18" value="The GDT-Server is only reachable internally.<br><br>Another community cannot query it and therefore can not determin the GDT-Balance or the GDT-Transactions of a user. Since this data is queried via EMail identificator it is not possible to expose the GDT-Server to 3rd party communties for obvious privacy reasons.<br><br>We need another process to expose the GDT data in a private manner, so the user data is protected and the GDT-Server can be exposed." style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=top;whiteSpace=wrap;rounded=0;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="10" y="180" width="260" height="180" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="22" value="Community Gradido Akademie" style="rounded=0;whiteSpace=wrap;html=1;align=left;verticalAlign=top;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="280" y="440" width="240" height="320" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="25" value="Suggested Process" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=21;fontStyle=1;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry y="410" width="200" height="20" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="26" value="IdentifierHash" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;startArrow=classic;startFill=1;strokeWidth=3;labelBackgroundColor=none;strokeColor=#2D7600;fontColor=#1D3557;fillColor=#60a917;" parent="1" source="27" target="28" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="27" value="Backend" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="320" y="480" width="160" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="28" value="GDT-Server" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="320" y="640" width="160" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="29" value="Community B" style="rounded=0;whiteSpace=wrap;html=1;align=left;verticalAlign=top;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="600" y="440" width="240" height="320" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="30" value="IdentifierHash" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;endArrow=classic;endFill=1;strokeWidth=3;labelBackgroundColor=none;strokeColor=#2D7600;fontColor=#1D3557;fillColor=#60a917;" parent="1" source="31" target="28" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="31" value="Backend" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="640" y="480" width="160" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="34" value="We expose the GDT-Server to the public, but allow the query of the GDT data by an anonymous identifier hash only.<br>This way the data can not be linked with a user and crawling of known EMail adresses is no longer possible.<br><br>This requires us to provide an user a way to get his identifier hash and our Gradido Akademie Community to automatically obtain it.<br><br>The following shall describe thoses processes." style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=top;whiteSpace=wrap;rounded=0;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="10" y="580" width="260" height="180" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="37" value="" style="endArrow=none;html=1;fontSize=21;strokeWidth=3;fillColor=none;labelBackgroundColor=#F1FAEE;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint y="400" as="sourcePoint"/>
|
||||
<mxPoint x="1090" y="400" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="38" value="" style="endArrow=none;html=1;fontSize=21;strokeWidth=3;fillColor=none;labelBackgroundColor=#F1FAEE;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint y="800" as="sourcePoint"/>
|
||||
<mxPoint x="1090" y="800" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="39" value="User requests his identifier hash" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=21;fontStyle=1;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry y="810" width="330" height="20" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="59" value="<font style="font-size: 12px ; line-height: 120%">Requests identifier hash <br>in his community's frontend</font>" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=21;startArrow=none;startFill=0;endArrow=classic;endFill=1;strokeWidth=3;jumpSize=6;startSize=6;spacing=2;labelBackgroundColor=none;strokeColor=#2D7600;fontColor=#1D3557;fillColor=#60a917;exitX=0.5;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="52" target="58" edge="1">
|
||||
<mxGeometry x="0.0041" y="-29" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="52" value="User" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontSize=21;strokeWidth=2;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="1049" y="889" width="30" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="62" value="<font style="font-size: 12px">Forward<br>with EMail<br>from user<br></font>" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=6;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.25;entryDx=0;entryDy=0;fontSize=21;startArrow=none;startFill=0;endArrow=classic;endFill=1;startSize=6;strokeWidth=3;labelBackgroundColor=none;strokeColor=#2D7600;fontColor=#1D3557;fillColor=#60a917;" parent="1" source="58" target="61" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="58" value="Backend" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="640" y="879" width="160" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="64" value="<font style="font-size: 12px">GDT-Server sends EMail with identifier hash</font>" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=6;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0.5;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;fontSize=21;startArrow=none;startFill=0;endArrow=classic;endFill=1;startSize=6;strokeWidth=3;labelBackgroundColor=none;strokeColor=#2D7600;fontColor=#1D3557;fillColor=#60a917;" parent="1" source="61" target="63" edge="1">
|
||||
<mxGeometry x="-0.0051" y="-11" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="61" value="GDT-Server" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="320" y="1040" width="160" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="63" value="User" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontSize=21;strokeWidth=2;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="1049" y="1070" width="30" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="69" value="The user can request an identifier hash via his community's frontend.<br>This will task the community's backend to send a request to the Gradido Akademie's GDT-Server with the EMail of the user.<br>The GDT-Server in turn will send the user his identifier hash via EMail.<br>The user can input this identifier hash into his community's frontend and therefore allows the foreign community to be able to query the GDT data.<br><br>Attributes of the identifier hash:<br>1. Not guessable: long, random, not just a hash of the EMail<br>2. Unique: Each user can have only one globally unique identifier<br>3. Persistent: The user might want to use the identifier hash on more then one service<br>4. Regeneratable: The user might want to request to replace his existing hash with a new one in case the old got compromised" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=top;whiteSpace=wrap;rounded=0;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="10" y="840" width="260" height="320" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="70" value="" style="endArrow=none;html=1;fontSize=21;strokeWidth=3;fillColor=none;labelBackgroundColor=#F1FAEE;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint y="1200" as="sourcePoint"/>
|
||||
<mxPoint x="1090" y="1200" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="71" value="Special case: Gradido Akademie Community" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=21;fontStyle=1;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry y="1210" width="450" height="20" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="72" value="Community Gradido Akademie" style="rounded=0;whiteSpace=wrap;html=1;align=left;verticalAlign=top;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="280" y="1240" width="240" height="480" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="73" value="Backend" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="320" y="1280" width="160" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="76" value="<font style="font-size: 12px">Write identifier hash<br>directly into Database</font>" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=6;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;fontSize=21;startArrow=none;startFill=0;endArrow=classic;endFill=1;startSize=6;strokeWidth=3;labelBackgroundColor=none;strokeColor=#B09500;fontColor=#1D3557;fillColor=#e3c800;" parent="1" source="74" target="73" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="74" value="GDT-Server" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="320" y="1440" width="160" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="75" value="Since the Gradido Akademie has access to all data it is not required to pester the user with an EMail confirmation process.<br><br>Therefore we can define two processes to keep all data up to date:<br><br>1. A donatation or other change to the GDT account of an EMail happens.<br>2. Continously write all existing data<br><br>Process 1 is dynamic and esures that once a user donates his identifier hash is directly written into the database as soon as this information reaches us.<br><br>Process 2 is in case a user registers with our service who already has donated in the past (legacy). This process is not syncronous, meaning it might take a moment till the newly registred user gets access to his old donation data. Furthermore we can use this to syncronize our data between the two services initially and periodically to prevent out-of-sync-data." style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=top;whiteSpace=wrap;rounded=0;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="10" y="1240" width="260" height="480" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="80" value="<font style="font-size: 12px">User donates</font>" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=6;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=21;startArrow=none;startFill=0;endArrow=classic;endFill=1;startSize=6;strokeWidth=3;labelBackgroundColor=none;strokeColor=#2D7600;fontColor=#1D3557;fillColor=#60a917;exitX=0.5;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="77" target="79" edge="1">
|
||||
<mxGeometry x="0.0323" y="-20" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="77" value="User" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontSize=21;strokeWidth=2;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="1048" y="1450" width="30" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="81" value="<font style="font-size: 12px">webhook</font>" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=6;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;fontSize=21;startArrow=none;startFill=0;endArrow=classic;endFill=1;startSize=6;strokeWidth=3;entryX=1;entryY=0.5;entryDx=0;entryDy=0;labelBackgroundColor=none;strokeColor=#2D7600;fontColor=#1D3557;fillColor=#60a917;" parent="1" source="79" target="74" edge="1">
|
||||
<mxGeometry y="-20" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="79" value="<font style="font-size: 12px">Donation Service</font>" style="rounded=0;whiteSpace=wrap;html=1;fontSize=21;strokeWidth=1;align=center;verticalAlign=middle;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="640" y="1440" width="160" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="84" value="<font style="font-size: 12px">Hook</font>" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=6;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;fontSize=21;startArrow=none;startFill=0;endArrow=classic;endFill=1;startSize=6;strokeWidth=3;labelBackgroundColor=none;strokeColor=#2D7600;fontColor=#1D3557;fillColor=#60a917;" parent="1" source="83" target="74" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="83" value="<font style="font-size: 12px">Cron</font>" style="rounded=0;whiteSpace=wrap;html=1;fontSize=21;strokeWidth=1;align=center;verticalAlign=middle;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="320" y="1600" width="160" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="87" value="<span style="font-size: 12px">Request GDT-Data</span>" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=6;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=21;startArrow=none;startFill=0;endArrow=classic;endFill=1;startSize=6;strokeWidth=3;labelBackgroundColor=none;strokeColor=#2D7600;fontColor=#1D3557;fillColor=#60a917;" parent="1" source="85" target="6" edge="1">
|
||||
<mxGeometry x="0.0189" y="20" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="85" value="User" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontSize=21;strokeWidth=2;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="90" width="30" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="89" value="<span style="font-size: 12px;">Request GDT-Data</span>" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=6;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;fontSize=21;startArrow=none;startFill=0;endArrow=classic;endFill=1;startSize=6;strokeWidth=3;labelBackgroundColor=none;strokeColor=#2D7600;fontColor=#1D3557;fillColor=#60a917;" parent="1" source="88" target="27" edge="1">
|
||||
<mxGeometry x="0.0189" y="20" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="88" value="User" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontSize=21;strokeWidth=2;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="490" width="30" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="91" value="<font style="font-size: 12px">Request GDT-Data</font>" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=6;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=21;startArrow=none;startFill=0;endArrow=classic;endFill=1;startSize=6;strokeWidth=3;labelBackgroundColor=none;strokeColor=#2D7600;fontColor=#1D3557;fillColor=#60a917;" parent="1" source="90" target="12" edge="1">
|
||||
<mxGeometry x="0.0189" y="-20" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="90" value="User" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontSize=21;strokeWidth=2;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="1050" y="90" width="30" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="93" value="<span style="font-size: 12px">Request GDT-Data</span>" style="edgeStyle=orthogonalEdgeStyle;rounded=1;jumpSize=6;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=21;startArrow=none;startFill=0;endArrow=classic;endFill=1;startSize=6;strokeWidth=3;labelBackgroundColor=none;strokeColor=#2D7600;fontColor=#1D3557;fillColor=#60a917;" parent="1" source="92" target="31" edge="1">
|
||||
<mxGeometry x="0.0189" y="-20" relative="1" as="geometry">
|
||||
<mxPoint as="offset"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="92" value="User" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;fontSize=21;strokeWidth=2;fillColor=#A8DADC;strokeColor=#457B9D;fontColor=#1D3557;" parent="1" vertex="1">
|
||||
<mxGeometry x="1050" y="490" width="30" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
BIN
docu/graphics/gdt_server_api.png
Normal file
BIN
docu/graphics/gdt_server_api.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 339 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
BIN
docu/graphics/mock_kontouebersicht.epgz
Normal file
BIN
docu/graphics/mock_kontouebersicht.epgz
Normal file
Binary file not shown.
BIN
docu/graphics/mock_kontouebersicht.png
Normal file
BIN
docu/graphics/mock_kontouebersicht.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
@ -222,7 +222,6 @@ with:
|
||||
```json
|
||||
{
|
||||
"session_id": -127182,
|
||||
"email": "max.musterman@gmail.de",
|
||||
"update": {
|
||||
"User.first_name": "Max",
|
||||
"User.last_name" : "Musterman",
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
LOGIN_API_URL=http://localhost/login_api/
|
||||
COMMUNITY_API_URL=http://localhost/api/
|
||||
ALLOW_REGISTER=true
|
||||
GRAPHQL_URI=http://localhost:4000/graphql
|
||||
//BUILD_COMMIT=0000000
|
||||
@ -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>
|
||||
|
||||
@ -14,7 +14,6 @@ export default {
|
||||
return {
|
||||
selected: null,
|
||||
options: [
|
||||
{ value: null, text: this.$t('select_language') },
|
||||
{ value: 'de', text: this.$t('languages.de') },
|
||||
{ value: 'en', text: this.$t('languages.en') },
|
||||
],
|
||||
|
||||
@ -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)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -18,8 +18,6 @@ const environment = {
|
||||
}
|
||||
|
||||
const server = {
|
||||
LOGIN_API_URL: process.env.LOGIN_API_URL || 'http://localhost/login_api/',
|
||||
COMMUNITY_API_URL: process.env.COMMUNITY_API_URL || 'http://localhost/api/',
|
||||
GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000/graphql',
|
||||
}
|
||||
|
||||
|
||||
9
frontend/src/graphql/enums.js
Normal file
9
frontend/src/graphql/enums.js
Normal 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',
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ export const login = gql`
|
||||
lastName
|
||||
language
|
||||
description
|
||||
coinanimation
|
||||
klickTipp {
|
||||
newsletterState
|
||||
}
|
||||
@ -32,8 +33,8 @@ export const loginViaEmailVerificationCode = gql`
|
||||
`
|
||||
|
||||
export const transactionsQuery = gql`
|
||||
query($firstPage: Int = 1, $items: Int = 25, $order: String = "DESC") {
|
||||
transactionList(firstPage: $firstPage, items: $items, order: $order) {
|
||||
query($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
|
||||
transactionList(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
|
||||
gdtSum
|
||||
count
|
||||
balance
|
||||
|
||||
@ -45,21 +45,14 @@
|
||||
"amount": "Betrag",
|
||||
"at": "am",
|
||||
"cancel": "Abbrechen",
|
||||
"change": "ändern",
|
||||
"change-name": "Name ändern",
|
||||
"change-password": "Passwort ändern",
|
||||
"changeLanguage": "Sprache ändern",
|
||||
"change_username_info": "Einmal gespeichert, kann der Username ncht mehr geändert werden!",
|
||||
"close": "schließen",
|
||||
"date": "Datum",
|
||||
"description": "Beschreibung",
|
||||
"edit": "bearbeiten",
|
||||
"email": "E-Mail",
|
||||
"email_repeat": "eMail wiederholen",
|
||||
"firstname": "Vorname",
|
||||
"from": "von",
|
||||
"lastname": "Nachname",
|
||||
"max_gdd_info": "Maximale anzahl GDD zum versenden erreicht!",
|
||||
"memo": "Nachricht",
|
||||
"message": "Nachricht",
|
||||
"password": "Passwort",
|
||||
@ -81,7 +74,6 @@
|
||||
"time": "Zeit",
|
||||
"to": "bis",
|
||||
"to1": "an",
|
||||
"username": "Username",
|
||||
"validation": {
|
||||
"gddSendAmount": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein",
|
||||
"is-not": "Du kannst dir selbst keine Gradidos überweisen",
|
||||
@ -106,29 +98,45 @@
|
||||
},
|
||||
"imprint": "Impressum",
|
||||
"language": "Sprache",
|
||||
"languages": {
|
||||
"de": "Deutsch",
|
||||
"en": "English",
|
||||
"success": "Deine Sprache wurde erfolgreich geändert."
|
||||
},
|
||||
"login": "Anmeldung",
|
||||
"logout": "Abmelden",
|
||||
"members_area": "Mitgliederbereich",
|
||||
"message": "hallo gradido !!",
|
||||
"privacy_policy": "Datenschutzerklärung",
|
||||
"reset": "Passwort zurücksetzen",
|
||||
"reset-password": {
|
||||
"not-authenticated": "Leider konnten wir dich nicht authentifizieren. Bitte wende dich an den Support.",
|
||||
"text": "Jetzt kannst du ein neues Passwort speichern, mit dem du dich zukünftig in der Gradido-App anmelden kannst.",
|
||||
"title": "Passwort zurücksetzen"
|
||||
},
|
||||
"select_language": "Bitte wähle eine Sprache für die App und Newsletter",
|
||||
"send": "Senden",
|
||||
"setting": {
|
||||
"changeNewsletter": "Newsletter Status ändern",
|
||||
"newsletter": "Newsletter",
|
||||
"newsletterFalse": "Du bist aus Newslettersystem ausgetragen.",
|
||||
"newsletterTrue": "Du bist im Newslettersystem eingetraten."
|
||||
"settings": {
|
||||
"coinanimation": {
|
||||
"coinanimation": "Münzanimation",
|
||||
"False": "Münzanimation ausgeschaltet",
|
||||
"True": "Münzanimation eingeschaltet"
|
||||
},
|
||||
"language": {
|
||||
"changeLanguage": "Sprache ändern",
|
||||
"de": "Deutsch",
|
||||
"en": "English",
|
||||
"select_language": "Bitte wähle eine Sprache.",
|
||||
"success": "Deine Sprache wurde erfolgreich geändert."
|
||||
},
|
||||
"name": {
|
||||
"change-name": "Name ändern",
|
||||
"change-success": "Dein Name wurde erfolgreich geändert."
|
||||
},
|
||||
"newsletter": {
|
||||
"newsletter": "Newsletter",
|
||||
"newsletterFalse": "Du bist aus Newslettersystem ausgetragen.",
|
||||
"newsletterTrue": "Du bist im Newslettersystem eingetraten."
|
||||
},
|
||||
"password": {
|
||||
"change-password": "Passwort ändern",
|
||||
"forgot_pwd": "Passwort vergessen?",
|
||||
"reset": "Passwort zurücksetzen",
|
||||
"reset-password": {
|
||||
"not-authenticated": "Leider konnten wir dich nicht authentifizieren. Bitte wende dich an den Support.",
|
||||
"text": "Jetzt kannst du ein neues Passwort speichern, mit dem du dich zukünftig in der Gradido-App anmelden kannst."
|
||||
},
|
||||
"send_now": "Jetzt senden",
|
||||
"subtitle": "Wenn du dein Passwort vergessen hast, kannst du es hier zurücksetzen."
|
||||
}
|
||||
},
|
||||
"signup": "Registrieren",
|
||||
"site": {
|
||||
@ -143,28 +151,13 @@
|
||||
},
|
||||
"login": {
|
||||
"community": "Tausend Dank, weil du bei uns bist!",
|
||||
"forgot_pwd": "Passwort vergessen?",
|
||||
"new_wallet": "Neues Konto erstellen",
|
||||
"remember": "Passwort merken",
|
||||
"signin": "Anmelden"
|
||||
},
|
||||
"navbar": {
|
||||
"activity": "Aktivität",
|
||||
"my-profil": "Mein Profil",
|
||||
"settings": "Einstellungen",
|
||||
"support": "Support"
|
||||
},
|
||||
"overview": {
|
||||
"account_overview": "Kontoübersicht",
|
||||
"add_work": "neuer Gemeinschaftsbeitrag",
|
||||
"send_gradido": "Gradido versenden",
|
||||
"since_last_month": "seid letzten Monat"
|
||||
},
|
||||
"password": {
|
||||
"send_now": "Jetzt senden",
|
||||
"subtitle": "Wenn du dein Passwort vergessen hast, kannst du es hier zurücksetzen.",
|
||||
"title": "Passwort zurücksetzen"
|
||||
},
|
||||
"signup": {
|
||||
"agree": "Ich stimme der <a href='https://gradido.net/de/datenschutz/' target='_blank' >Datenschutzerklärung</a> zu.",
|
||||
"dont_match": "Die Passwörter stimmen nicht überein.",
|
||||
@ -194,6 +187,5 @@
|
||||
"show_all": "Alle <strong>{count}</strong> Transaktionen ansehen"
|
||||
},
|
||||
"transactions": "Transaktionen",
|
||||
"welcome": "Willkommen!",
|
||||
"whitepaper": "Whitepaper"
|
||||
}
|
||||
|
||||
@ -45,21 +45,14 @@
|
||||
"amount": "Amount",
|
||||
"at": "at",
|
||||
"cancel": "Cancel",
|
||||
"change": "change",
|
||||
"change-name": "Change name",
|
||||
"change-password": "Change password",
|
||||
"changeLanguage": "Change language",
|
||||
"change_username_info": "Once saved, the username cannot be changed again!",
|
||||
"close": "Close",
|
||||
"date": "Date",
|
||||
"description": "Description",
|
||||
"edit": "Edit",
|
||||
"email": "Email",
|
||||
"email_repeat": "Repeat Email",
|
||||
"firstname": "Firstname",
|
||||
"from": "from",
|
||||
"lastname": "Lastname",
|
||||
"max_gdd_info": "Maximum number of GDDs to be sent has been reached!",
|
||||
"memo": "Message",
|
||||
"message": "Message",
|
||||
"password": "Password",
|
||||
@ -81,7 +74,6 @@
|
||||
"time": "Time",
|
||||
"to": "to",
|
||||
"to1": "to",
|
||||
"username": "Username",
|
||||
"validation": {
|
||||
"gddSendAmount": "The {_field_} field must be a number between {min} and {max} with at most two digits",
|
||||
"is-not": "You cannot send Gradidos to yourself",
|
||||
@ -106,29 +98,45 @@
|
||||
},
|
||||
"imprint": "Legal notice",
|
||||
"language": "Language",
|
||||
"languages": {
|
||||
"de": "Deutsch",
|
||||
"en": "English",
|
||||
"success": "Your language has been successfully updated."
|
||||
},
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"members_area": "Member's area",
|
||||
"message": "hello gradido !!",
|
||||
"privacy_policy": "Privacy policy",
|
||||
"reset": "Reset password",
|
||||
"reset-password": {
|
||||
"not-authenticated": "Unfortunately we could not authenticate you. Please contact the support.",
|
||||
"text": "Now you can save a new password to login to the Gradido-App in the future.",
|
||||
"title": "Reset Password"
|
||||
},
|
||||
"select_language": "Please choose a language for the app and newsletter",
|
||||
"send": "Send",
|
||||
"setting": {
|
||||
"changeNewsletter": "Newsletter status change",
|
||||
"newsletter": "Newsletter",
|
||||
"newsletterFalse": "You are unsubscribed from newsletter system.",
|
||||
"newsletterTrue": "You are subscribed to newsletter system."
|
||||
"settings": {
|
||||
"coinanimation": {
|
||||
"coinanimation": "Coin animation",
|
||||
"False": "Coin animation disabled",
|
||||
"True": "Coin animation enabled"
|
||||
},
|
||||
"language": {
|
||||
"changeLanguage": "Change language",
|
||||
"de": "Deutsch",
|
||||
"en": "English",
|
||||
"select_language": "Please choose a language.",
|
||||
"success": "Your language has been successfully updated."
|
||||
},
|
||||
"name": {
|
||||
"change-name": "Change name",
|
||||
"change-success": "Your name has been successfully changed."
|
||||
},
|
||||
"newsletter": {
|
||||
"newsletter": "Newsletter",
|
||||
"newsletterFalse": "You are unsubscribed from newsletter system.",
|
||||
"newsletterTrue": "You are subscribed to newsletter system."
|
||||
},
|
||||
"password": {
|
||||
"change-password": "Change password",
|
||||
"forgot_pwd": "Forgot password?",
|
||||
"reset": "Reset password",
|
||||
"reset-password": {
|
||||
"not-authenticated": "Unfortunately we could not authenticate you. Please contact the support.",
|
||||
"text": "Now you can save a new password to login to the Gradido-App in the future."
|
||||
},
|
||||
"send_now": "Send now",
|
||||
"subtitle": "If you have forgotten your password, you can reset it here."
|
||||
}
|
||||
},
|
||||
"signup": "Sign up",
|
||||
"site": {
|
||||
@ -143,28 +151,13 @@
|
||||
},
|
||||
"login": {
|
||||
"community": "A thousand thanks for being with us!",
|
||||
"forgot_pwd": "Forgot password?",
|
||||
"new_wallet": "Create new account",
|
||||
"remember": "Remember password",
|
||||
"signin": "Sign in"
|
||||
},
|
||||
"navbar": {
|
||||
"activity": "Activity",
|
||||
"my-profil": "My profile",
|
||||
"settings": "Settings",
|
||||
"support": "Support"
|
||||
},
|
||||
"overview": {
|
||||
"account_overview": "Account overview",
|
||||
"add_work": "New Community Contribution",
|
||||
"send_gradido": "Send Gradido",
|
||||
"since_last_month": "since last month"
|
||||
},
|
||||
"password": {
|
||||
"send_now": "Send now",
|
||||
"subtitle": "If you have forgotten your password, you can reset it here.",
|
||||
"title": "Reset password"
|
||||
},
|
||||
"signup": {
|
||||
"agree": "I agree to the <a href='https://gradido.net/en/datenschutz/' target='_blank' > privacy policy</a>.",
|
||||
"dont_match": "Passwords don't match.",
|
||||
@ -194,6 +187,5 @@
|
||||
"show_all": "View all <strong>{count}</strong> transactions."
|
||||
},
|
||||
"transactions": "Transactions",
|
||||
"welcome": "Welcome!",
|
||||
"whitepaper": "Whitepaper"
|
||||
}
|
||||
|
||||
@ -67,6 +67,7 @@ export const store = new Vuex.Store({
|
||||
username: '',
|
||||
description: '',
|
||||
token: null,
|
||||
coinanimation: true,
|
||||
newsletterState: null,
|
||||
},
|
||||
getters: {},
|
||||
|
||||
@ -114,14 +114,14 @@ export const loadAllRules = (i18nCallback) => {
|
||||
|
||||
extend('atLeastOneSpecialCharater', {
|
||||
validate(value) {
|
||||
return !!value.match(/[^a-zA-Z0-9]/)
|
||||
return !!value.match(/[^a-zA-Z0-9 \t\n\r]/)
|
||||
},
|
||||
message: (_, values) => i18nCallback.t('site.signup.special-char', values),
|
||||
})
|
||||
|
||||
extend('noWhitespaceCharacters', {
|
||||
validate(value) {
|
||||
return !!value.match(/[^ \t\n\r]/)
|
||||
return !value.match(/[ \t\n\r]+/)
|
||||
},
|
||||
message: (_, values) => i18nCallback.t('site.signup.no-whitespace', values),
|
||||
})
|
||||
|
||||
@ -32,6 +32,9 @@ describe('DashboardLayoutGdd', () => {
|
||||
},
|
||||
$router: {
|
||||
push: routerPushMock,
|
||||
currentRoute: {
|
||||
path: '/overview',
|
||||
},
|
||||
},
|
||||
$toasted: {
|
||||
error: toasterMock,
|
||||
@ -143,21 +146,23 @@ describe('DashboardLayoutGdd', () => {
|
||||
it('redirects to login page', () => {
|
||||
expect(routerPushMock).toBeCalledWith('/login')
|
||||
})
|
||||
})
|
||||
|
||||
describe('logout fails', () => {
|
||||
beforeEach(() => {
|
||||
apolloMock.mockRejectedValue({
|
||||
message: 'error',
|
||||
})
|
||||
describe('logout fails', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMock.mockRejectedValue({
|
||||
message: 'error',
|
||||
})
|
||||
await wrapper.findComponent({ name: 'sidebar' }).vm.$emit('logout')
|
||||
await flushPromises()
|
||||
})
|
||||
|
||||
it('dispatches logout to store', () => {
|
||||
expect(storeDispatchMock).toBeCalledWith('logout')
|
||||
})
|
||||
it('dispatches logout to store', () => {
|
||||
expect(storeDispatchMock).toBeCalledWith('logout')
|
||||
})
|
||||
|
||||
it('redirects to login page', () => {
|
||||
expect(routerPushMock).toBeCalledWith('/login')
|
||||
})
|
||||
it('redirects to login page', () => {
|
||||
expect(routerPushMock).toBeCalledWith('/login')
|
||||
})
|
||||
})
|
||||
|
||||
@ -184,7 +189,7 @@ describe('DashboardLayoutGdd', () => {
|
||||
})
|
||||
await wrapper
|
||||
.findComponent({ ref: 'router-view' })
|
||||
.vm.$emit('update-transactions', { firstPage: 2, items: 5 })
|
||||
.vm.$emit('update-transactions', { currentPage: 2, pageSize: 5 })
|
||||
await flushPromises()
|
||||
})
|
||||
|
||||
@ -192,8 +197,8 @@ describe('DashboardLayoutGdd', () => {
|
||||
expect(apolloMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
firstPage: 2,
|
||||
items: 5,
|
||||
currentPage: 2,
|
||||
pageSize: 5,
|
||||
},
|
||||
}),
|
||||
)
|
||||
@ -228,7 +233,7 @@ describe('DashboardLayoutGdd', () => {
|
||||
})
|
||||
await wrapper
|
||||
.findComponent({ ref: 'router-view' })
|
||||
.vm.$emit('update-transactions', { firstPage: 2, items: 5 })
|
||||
.vm.$emit('update-transactions', { currentPage: 2, pageSize: 5 })
|
||||
await flushPromises()
|
||||
})
|
||||
|
||||
|
||||
@ -101,7 +101,7 @@ export default {
|
||||
.catch(() => {
|
||||
this.$sidebar.displaySidebar(false)
|
||||
this.$store.dispatch('logout')
|
||||
this.$router.push('/login')
|
||||
if (this.$router.currentRoute.path !== '/login') this.$router.push('/login')
|
||||
})
|
||||
},
|
||||
async updateTransactions(pagination) {
|
||||
@ -110,8 +110,8 @@ export default {
|
||||
.query({
|
||||
query: transactionsQuery,
|
||||
variables: {
|
||||
firstPage: pagination.firstPage,
|
||||
items: pagination.items,
|
||||
currentPage: pagination.currentPage,
|
||||
pageSize: pagination.pageSize,
|
||||
},
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
|
||||
@ -54,7 +54,7 @@ describe('GddTransactionList', () => {
|
||||
await wrapper.setProps({
|
||||
transactions: [
|
||||
{
|
||||
balance: '19.93',
|
||||
balance: 19.93,
|
||||
date: '2021-05-25T17:38:13+00:00',
|
||||
memo: 'Alles Gute zum Geburtstag',
|
||||
name: 'Bob der Baumeister',
|
||||
@ -63,7 +63,7 @@ describe('GddTransactionList', () => {
|
||||
decay: { balance: '0.5' },
|
||||
},
|
||||
{
|
||||
balance: '1000',
|
||||
balance: 1000,
|
||||
date: '2021-04-29T15:34:49+00:00',
|
||||
memo: 'Gut das du da bist!',
|
||||
name: 'Gradido Akademie',
|
||||
@ -71,7 +71,7 @@ describe('GddTransactionList', () => {
|
||||
type: 'creation',
|
||||
},
|
||||
{
|
||||
balance: '314.98',
|
||||
balance: 314.98,
|
||||
date: '2021-04-29T17:26:40+00:00',
|
||||
memo: 'Für das Fahrrad!',
|
||||
name: 'Jan Ulrich',
|
||||
@ -327,7 +327,9 @@ describe('GddTransactionList', () => {
|
||||
|
||||
it('emits update-transactions when next button is clicked', async () => {
|
||||
await paginationButtons.find('button.next-page').trigger('click')
|
||||
expect(wrapper.emitted('update-transactions')[1]).toEqual([{ firstPage: 2, items: 25 }])
|
||||
expect(wrapper.emitted('update-transactions')[1]).toEqual([
|
||||
{ currentPage: 2, pageSize: 25 },
|
||||
])
|
||||
})
|
||||
|
||||
it('shows text "2 / 2" when next button is clicked', async () => {
|
||||
@ -348,7 +350,9 @@ describe('GddTransactionList', () => {
|
||||
it('emits update-transactions when preivous button is clicked after next buton', async () => {
|
||||
await paginationButtons.find('button.next-page').trigger('click')
|
||||
await paginationButtons.find('button.previous-page').trigger('click')
|
||||
expect(wrapper.emitted('update-transactions')[2]).toEqual([{ firstPage: 1, items: 25 }])
|
||||
expect(wrapper.emitted('update-transactions')[2]).toEqual([
|
||||
{ currentPage: 1, pageSize: 25 },
|
||||
])
|
||||
expect(scrollToMock).toBeCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -134,8 +134,8 @@ export default {
|
||||
methods: {
|
||||
updateTransactions() {
|
||||
this.$emit('update-transactions', {
|
||||
firstPage: this.currentPage,
|
||||
items: this.pageSize,
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
window.scrollTo(0, 0)
|
||||
},
|
||||
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -39,11 +39,11 @@ describe('ForgotPassword', () => {
|
||||
})
|
||||
|
||||
it('has a title', () => {
|
||||
expect(wrapper.find('h1').text()).toEqual('site.password.title')
|
||||
expect(wrapper.find('h1').text()).toEqual('settings.password.reset')
|
||||
})
|
||||
|
||||
it('has a subtitle', () => {
|
||||
expect(wrapper.find('p.text-lead').text()).toEqual('site.password.subtitle')
|
||||
expect(wrapper.find('p.text-lead').text()).toEqual('settings.password.subtitle')
|
||||
})
|
||||
|
||||
describe('back button', () => {
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
<div class="header-body text-center mb-7">
|
||||
<b-row class="justify-content-center">
|
||||
<b-col xl="5" lg="6" md="8" class="px-2">
|
||||
<h1>{{ $t('site.password.title') }}</h1>
|
||||
<p class="text-lead">{{ $t('site.password.subtitle') }}</p>
|
||||
<h1>{{ $t('settings.password.reset') }}</h1>
|
||||
<p class="text-lead">{{ $t('settings.password.subtitle') }}</p>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
@ -22,7 +22,7 @@
|
||||
<input-email v-model="form.email"></input-email>
|
||||
<div class="text-center">
|
||||
<b-button type="submit" variant="primary">
|
||||
{{ $t('site.password.send_now') }}
|
||||
{{ $t('settings.password.send_now') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-form>
|
||||
|
||||
@ -71,7 +71,7 @@ describe('Login', () => {
|
||||
describe('links', () => {
|
||||
it('has a link "Forgot Password?"', () => {
|
||||
expect(wrapper.findAllComponents(RouterLinkStub).at(0).text()).toEqual(
|
||||
'site.login.forgot_pwd',
|
||||
'settings.password.forgot_pwd',
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@
|
||||
<b-row class="mt-3">
|
||||
<b-col cols="6">
|
||||
<router-link to="/password">
|
||||
{{ $t('site.login.forgot_pwd') }}
|
||||
{{ $t('settings.password.forgot_pwd') }}
|
||||
</router-link>
|
||||
</b-col>
|
||||
<b-col cols="6" class="text-right" v-show="allowRegister">
|
||||
|
||||
@ -25,7 +25,7 @@ describe('Register', () => {
|
||||
$store: {
|
||||
state: {
|
||||
email: 'peter@lustig.de',
|
||||
language: null,
|
||||
language: 'en',
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -55,11 +55,11 @@ describe('Register', () => {
|
||||
|
||||
describe('links', () => {
|
||||
it('has a link "Back"', () => {
|
||||
expect(wrapper.findAllComponents(RouterLinkStub).at(0).text()).toEqual('back')
|
||||
expect(wrapper.find('.test-button-back').text()).toEqual('back')
|
||||
})
|
||||
|
||||
it('links to /login when clicking "Back"', () => {
|
||||
expect(wrapper.findAllComponents(RouterLinkStub).at(0).props().to).toBe('/login')
|
||||
expect(wrapper.find('.test-button-back').props().to).toBe('/login')
|
||||
})
|
||||
})
|
||||
|
||||
@ -89,17 +89,17 @@ describe('Register', () => {
|
||||
it('has Language selected field', () => {
|
||||
expect(wrapper.find('.selectedLanguage').exists()).toBeTruthy()
|
||||
})
|
||||
it('selected Language value de', async () => {
|
||||
it('selects Language value en', async () => {
|
||||
wrapper.find('.selectedLanguage').findAll('option').at(1).setSelected()
|
||||
expect(wrapper.find('.selectedLanguage').element.value).toBe('de')
|
||||
expect(wrapper.find('.selectedLanguage').element.value).toBe('en')
|
||||
})
|
||||
|
||||
it('has 1 checkbox input fields', () => {
|
||||
expect(wrapper.find('#registerCheckbox').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has no submit button when not completely filled', () => {
|
||||
expect(wrapper.find('button[type="submit"]').exists()).toBe(false)
|
||||
it('has disabled submit button when not completely filled', () => {
|
||||
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBe('disabled')
|
||||
})
|
||||
|
||||
it('displays a message that Email is required', async () => {
|
||||
@ -127,60 +127,6 @@ describe('Register', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('resetForm', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.find('#registerFirstname').setValue('Max')
|
||||
wrapper.find('#registerLastname').setValue('Mustermann')
|
||||
wrapper.find('#Email-input-field').setValue('max.mustermann@gradido.net')
|
||||
wrapper.find('input[name="form.password"]').setValue('Aa123456_')
|
||||
wrapper.find('input[name="form.passwordRepeat"]').setValue('Aa123456_')
|
||||
wrapper.find('.language-switch-select').findAll('option').at(1).setSelected()
|
||||
wrapper.find('input[name="site.signup.agree"]').setChecked(true)
|
||||
})
|
||||
|
||||
it('reset selected value language', async () => {
|
||||
await wrapper.find('button.ml-2').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('.language-switch-select').element.value).toBe(undefined)
|
||||
})
|
||||
|
||||
it('resets the firstName field after clicking the reset button', async () => {
|
||||
await wrapper.find('button.ml-2').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('#registerFirstname').element.value).toBe('')
|
||||
})
|
||||
|
||||
it('resets the lastName field after clicking the reset button', async () => {
|
||||
await wrapper.find('button.ml-2').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('#registerLastname').element.value).toBe('')
|
||||
})
|
||||
|
||||
it('resets the email field after clicking the reset button', async () => {
|
||||
await wrapper.find('button.ml-2').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('#Email-input-field').element.value).toBe('')
|
||||
})
|
||||
|
||||
it.skip('resets the password field after clicking the reset button', async () => {
|
||||
await wrapper.find('button.ml-2').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('input[name="form.password"]').element.value).toBe('')
|
||||
})
|
||||
|
||||
it.skip('resets the passwordRepeat field after clicking the reset button', async () => {
|
||||
await wrapper.find('button.ml-2').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('input[name="form.passwordRepeat"]').element.value).toBe('')
|
||||
})
|
||||
|
||||
it('resets the firstName field after clicking the reset button', async () => {
|
||||
await wrapper.find('button.ml-2').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('input[name="site.signup.agree"]').props.checked).not.toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('API calls', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.find('#registerFirstname').setValue('Max')
|
||||
@ -191,6 +137,10 @@ describe('Register', () => {
|
||||
wrapper.find('.language-switch-select').findAll('option').at(1).setSelected()
|
||||
})
|
||||
|
||||
it('has enabled submit button when completely filled', () => {
|
||||
expect(wrapper.find('button[type="submit"]').attributes('disabled')).toBe('disabled')
|
||||
})
|
||||
|
||||
describe('server sends back error', () => {
|
||||
beforeEach(async () => {
|
||||
registerUserMutationMock.mockRejectedValue({ message: 'Ouch!' })
|
||||
@ -235,7 +185,7 @@ describe('Register', () => {
|
||||
firstName: 'Max',
|
||||
lastName: 'Mustermann',
|
||||
password: 'Aa123456_',
|
||||
language: 'de',
|
||||
language: 'en',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
@ -116,13 +116,19 @@
|
||||
</span>
|
||||
</b-alert>
|
||||
|
||||
<div
|
||||
class="text-center"
|
||||
v-if="namesFilled && emailFilled && form.agree && languageFilled"
|
||||
>
|
||||
<div class="text-center">
|
||||
<div class="text-center">
|
||||
<b-button class="ml-2" @click="resetForm()">{{ $t('form.reset') }}</b-button>
|
||||
<b-button type="submit" variant="primary">{{ $t('signup') }}</b-button>
|
||||
<b-button class="ml-2 test-button-back" to="/login">
|
||||
{{ $t('back') }}
|
||||
</b-button>
|
||||
|
||||
<b-button
|
||||
:disabled="!(namesFilled && emailFilled && form.agree && languageFilled)"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
>
|
||||
{{ $t('signup') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</b-form>
|
||||
@ -131,9 +137,6 @@
|
||||
</b-card>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<div class="text-center py-lg-4">
|
||||
<router-link to="/login" class="mt-3">{{ $t('back') }}</router-link>
|
||||
</div>
|
||||
</b-container>
|
||||
</div>
|
||||
</template>
|
||||
@ -172,22 +175,6 @@ export default {
|
||||
getValidationState({ dirty, validated, valid = null }) {
|
||||
return dirty || validated ? valid : null
|
||||
},
|
||||
resetForm() {
|
||||
this.form = {
|
||||
firstname: '',
|
||||
lastname: '',
|
||||
email: '',
|
||||
password: {
|
||||
password: '',
|
||||
passwordRepeat: '',
|
||||
},
|
||||
agree: false,
|
||||
}
|
||||
this.language = ''
|
||||
this.$nextTick(() => {
|
||||
this.$refs.observer.reset()
|
||||
})
|
||||
},
|
||||
async onSubmit() {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
@ -238,7 +225,7 @@ export default {
|
||||
return this.form.email !== ''
|
||||
},
|
||||
languageFilled() {
|
||||
return this.language !== null && this.language !== ''
|
||||
return !!this.language
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -71,8 +71,10 @@ describe('ResetPassword', () => {
|
||||
})
|
||||
|
||||
it('has a message suggesting to contact the support', () => {
|
||||
expect(wrapper.find('div.header').text()).toContain('reset-password.title')
|
||||
expect(wrapper.find('div.header').text()).toContain('reset-password.not-authenticated')
|
||||
expect(wrapper.find('div.header').text()).toContain('settings.password.reset')
|
||||
expect(wrapper.find('div.header').text()).toContain(
|
||||
'settings.password.reset-password.not-authenticated',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -99,8 +101,10 @@ describe('ResetPassword', () => {
|
||||
|
||||
describe('Register header', () => {
|
||||
it('has a welcome message', async () => {
|
||||
expect(wrapper.find('div.header').text()).toContain('reset-password.title')
|
||||
expect(wrapper.find('div.header').text()).toContain('reset-password.text')
|
||||
expect(wrapper.find('div.header').text()).toContain('settings.password.reset')
|
||||
expect(wrapper.find('div.header').text()).toContain(
|
||||
'settings.password.reset-password.text',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -5,13 +5,13 @@
|
||||
<div class="header-body text-center mb-7">
|
||||
<b-row class="justify-content-center">
|
||||
<b-col xl="5" lg="6" md="8" class="px-2">
|
||||
<h1>{{ $t('reset-password.title') }}</h1>
|
||||
<h1>{{ $t('settings.password.reset') }}</h1>
|
||||
<div class="pb-4" v-if="!pending">
|
||||
<span v-if="authenticated">
|
||||
{{ $t('reset-password.text') }}
|
||||
{{ $t('settings.password.reset-password.text') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('reset-password.not-authenticated') }}
|
||||
{{ $t('settings.password.reset-password.not-authenticated') }}
|
||||
</span>
|
||||
</div>
|
||||
</b-col>
|
||||
@ -29,7 +29,7 @@
|
||||
<input-password-confirmation v-model="form" :register="register" />
|
||||
<div class="text-center">
|
||||
<b-button type="submit" variant="primary" class="mt-4">
|
||||
{{ $t('reset') }}
|
||||
{{ $t('settings.password.reset') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-form>
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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>
|
||||
@ -139,7 +139,7 @@ describe('UserCard_FormUserData', () => {
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessMock).toBeCalledWith('site.profil.user-data.change-success')
|
||||
expect(toastSuccessMock).toBeCalledWith('settings.name.change-success')
|
||||
})
|
||||
|
||||
it('has an edit button again', () => {
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<b-row class="mb-4 text-right">
|
||||
<b-col class="text-right">
|
||||
<a @click="showUserData ? (showUserData = !showUserData) : cancelEdit()">
|
||||
<span class="pointer mr-3">{{ $t('form.change-name') }}</span>
|
||||
<span class="pointer mr-3">{{ $t('settings.name.change-name') }}</span>
|
||||
<b-icon v-if="showUserData" class="pointer ml-3" icon="pencil"></b-icon>
|
||||
<b-icon v-else icon="x-circle" class="pointer ml-3" variant="danger"></b-icon>
|
||||
</a>
|
||||
@ -122,7 +122,7 @@ export default {
|
||||
this.$store.commit('lastName', this.form.lastName)
|
||||
this.$store.commit('description', this.form.description)
|
||||
this.showUserData = true
|
||||
this.$toasted.success(this.$t('site.profil.user-data.change-success'))
|
||||
this.$toasted.success(this.$t('settings.name.change-success'))
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user