diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ee602a343..3d046fcda 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -528,7 +528,7 @@ jobs: report_name: Coverage Backend type: lcov result_path: ./backend/coverage/lcov.info - min_coverage: 55 + min_coverage: 58 token: ${{ github.token }} ########################################################################## diff --git a/CHANGELOG.md b/CHANGELOG.md index 53376946c..5d8b71dac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,54 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [1.8.0](https://github.com/gradido/gradido/compare/1.7.1...1.8.0) + +- Fix: database version requirement for backend corrected [`#1835`](https://github.com/gradido/gradido/pull/1835) +- feat: More User Resolver Tests [`#1827`](https://github.com/gradido/gradido/pull/1827) +- fix: Round Decay with Tranasction Links [`#1834`](https://github.com/gradido/gradido/pull/1834) +- Fix: config value for the redeem URL was missing [`#1828`](https://github.com/gradido/gradido/pull/1828) +- Refactor: Database admin pending creations use decimal [`#1748`](https://github.com/gradido/gradido/pull/1748) +- refactor: Drop Server User Table [`#1808`](https://github.com/gradido/gradido/pull/1808) +- 1816 expired link are not highlighted [`#1821`](https://github.com/gradido/gradido/pull/1821) +- 1812 put qr code into popup on generate [`#1820`](https://github.com/gradido/gradido/pull/1820) +- Docu: Federation image [`#1817`](https://github.com/gradido/gradido/pull/1817) +- 1813 qr code popup [`#1819`](https://github.com/gradido/gradido/pull/1819) +- Fix: cross-env for windows [`#1822`](https://github.com/gradido/gradido/pull/1822) +- fix: Double Load Transaction Links [`#1818`](https://github.com/gradido/gradido/pull/1818) +- Generated link in backend should also give back the base url [`#1745`](https://github.com/gradido/gradido/pull/1745) +- 1731 style startDecayStartblock, style Adapted across pages [`#1809`](https://github.com/gradido/gradido/pull/1809) +- Refactor: Frontend bake in community info [`#1750`](https://github.com/gradido/gradido/pull/1750) +- fix: Load Transaction Link Details on Click [`#1806`](https://github.com/gradido/gradido/pull/1806) +- devops: Deploy Seed in Backend [`#1790`](https://github.com/gradido/gradido/pull/1790) +- refactor: Balance Model and Decay Rounding [`#1780`](https://github.com/gradido/gradido/pull/1780) +- change config DECAY_START_TIME in UTC 0000 [`#1807`](https://github.com/gradido/gradido/pull/1807) +- 1751 make gdt visible only if explicitly clicked [`#1752`](https://github.com/gradido/gradido/pull/1752) +- add Tab system from bootstrap in SearchUserTable Userdata [`#1744`](https://github.com/gradido/gradido/pull/1744) +- Fix: Certbot renewal [`#1789`](https://github.com/gradido/gradido/pull/1789) +- 🍰 Add Wallet Link To Mails [`#1765`](https://github.com/gradido/gradido/pull/1765) +- 1633 display qr code on link in transaction list [`#1661`](https://github.com/gradido/gradido/pull/1661) +- 1755 insert additional text when redeeming [`#1756`](https://github.com/gradido/gradido/pull/1756) +- refactor: Define Context Interface [`#1762`](https://github.com/gradido/gradido/pull/1762) +- fix: Elopage Status [`#1742`](https://github.com/gradido/gradido/pull/1742) +- Refactor: Frontend decay start block as static config value [`#1749`](https://github.com/gradido/gradido/pull/1749) +- better date format for reddem valid date [`#1758`](https://github.com/gradido/gradido/pull/1758) +- add insert shadow in summary links transaction type [`#1754`](https://github.com/gradido/gradido/pull/1754) +- Feature: JWT duration is now 30min by default [`#1747`](https://github.com/gradido/gradido/pull/1747) +- Docu: Scope of Gradido [`#1746`](https://github.com/gradido/gradido/pull/1746) +- fix: Check That Recipient User Has Activated Account to Receive Coins [`#1743`](https://github.com/gradido/gradido/pull/1743) +- Fix: Fixed config dist version to properly reflect new password reset url [`#1737`](https://github.com/gradido/gradido/pull/1737) +- 503 transaction list pagination pages clickable [`#1677`](https://github.com/gradido/gradido/pull/1677) +- if no recipientEmail else form.email [`#1722`](https://github.com/gradido/gradido/pull/1722) +- 1727 change button text and observe spelling [`#1728`](https://github.com/gradido/gradido/pull/1728) +- 1729 load spinner if pending balance [`#1730`](https://github.com/gradido/gradido/pull/1730) +- transaction type remains when jumping from the verification back [`#1724`](https://github.com/gradido/gradido/pull/1724) +- text for toast expand link copied [`#1726`](https://github.com/gradido/gradido/pull/1726) + #### [1.7.1](https://github.com/gradido/gradido/compare/1.7.0...1.7.1) +> 1 April 2022 + +- v1.7.1 [`#1721`](https://github.com/gradido/gradido/pull/1721) - fix: Localize Dates on Redeem Transaction Link Page [`#1720`](https://github.com/gradido/gradido/pull/1720) - fix: Round Virtual Transaction Link Transaction [`#1718`](https://github.com/gradido/gradido/pull/1718) - larger icon and deacy information if center [`#1719`](https://github.com/gradido/gradido/pull/1719) diff --git a/admin/package.json b/admin/package.json index ae1f1c305..c5b2e60f5 100644 --- a/admin/package.json +++ b/admin/package.json @@ -3,7 +3,7 @@ "description": "Administraion Interface for Gradido", "main": "index.js", "author": "Moriz Wahl", - "version": "1.7.1", + "version": "1.8.0", "license": "MIT", "private": false, "scripts": { @@ -14,7 +14,7 @@ "analyse-bundle": "yarn build && webpack-bundle-analyzer dist/webpack.stats.json", "lint": "eslint --max-warnings=0 --ext .js,.vue,.json .", "stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'", - "test": "TZ=UTC jest --coverage", + "test": "cross-env TZ=UTC jest --coverage", "locales": "scripts/sort.sh" }, "dependencies": { @@ -57,6 +57,7 @@ "@vue/cli-service": "~4.5.0", "babel-eslint": "^10.1.0", "babel-plugin-transform-require-context": "^0.1.1", + "cross-env": "^7.0.3", "eslint": "7.25.0", "eslint-config-prettier": "^8.3.0", "eslint-config-standard": "^16.0.3", diff --git a/admin/src/graphql/createPendingCreation.js b/admin/src/graphql/createPendingCreation.js index 183fa5b15..05402ed9f 100644 --- a/admin/src/graphql/createPendingCreation.js +++ b/admin/src/graphql/createPendingCreation.js @@ -3,7 +3,7 @@ import gql from 'graphql-tag' export const createPendingCreation = gql` mutation ( $email: String! - $amount: Float! + $amount: Decimal! $memo: String! $creationDate: String! $moderator: Int! diff --git a/admin/src/graphql/updatePendingCreation.js b/admin/src/graphql/updatePendingCreation.js index 77668f15b..cd0ae6c8e 100644 --- a/admin/src/graphql/updatePendingCreation.js +++ b/admin/src/graphql/updatePendingCreation.js @@ -4,7 +4,7 @@ export const updatePendingCreation = gql` mutation ( $id: Int! $email: String! - $amount: Float! + $amount: Decimal! $memo: String! $creationDate: String! $moderator: Int! diff --git a/admin/yarn.lock b/admin/yarn.lock index d5fae27fe..af1d18fa6 100644 --- a/admin/yarn.lock +++ b/admin/yarn.lock @@ -4688,6 +4688,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -4708,7 +4715,7 @@ cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.2: +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== diff --git a/backend/.env.dist b/backend/.env.dist index b0a08753c..de33a7272 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -1,4 +1,4 @@ -CONFIG_VERSION=v5.2022-04-12 +CONFIG_VERSION=v6.2022-04-21 # Server PORT=4000 @@ -27,6 +27,7 @@ KLICKTIPP_APIKEY_EN=SomeFakeKeyEN COMMUNITY_NAME=Gradido Entwicklung COMMUNITY_URL=http://localhost/ COMMUNITY_REGISTER_URL=http://localhost/register +COMMUNITY_REDEEM_URL=http://localhost/redeem/{code} COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido. # Login Server diff --git a/backend/.env.template b/backend/.env.template index 5da108f53..8ce8fca4e 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -26,6 +26,7 @@ KLICKTIPP_APIKEY_EN=$KLICKTIPP_APIKEY_EN COMMUNITY_NAME=$COMMUNITY_NAME COMMUNITY_URL=$COMMUNITY_URL COMMUNITY_REGISTER_URL=$COMMUNITY_REGISTER_URL +COMMUNITY_REDEEM_URL=$COMMUNITY_REDEEM_URL COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION # Login Server diff --git a/backend/package.json b/backend/package.json index 3c4066d6b..641a24601 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "gradido-backend", - "version": "1.7.1", + "version": "1.8.0", "description": "Gradido unified backend providing an API-Service for Gradido Transactions", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/backend", @@ -10,11 +10,11 @@ "scripts": { "build": "tsc --build", "clean": "tsc --build --clean", - "start": "TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js", - "dev": "TZ=UTC nodemon -w src --ext ts --exec ts-node -r tsconfig-paths/register src/index.ts", + "start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js", + "dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r tsconfig-paths/register src/index.ts", "lint": "eslint --max-warnings=0 --ext .js,.ts .", - "test": "TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles", - "seed": "TZ=UTC ts-node -r tsconfig-paths/register src/seeds/index.ts" + "test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles", + "seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts" }, "dependencies": { "@types/jest": "^27.0.2", @@ -25,6 +25,7 @@ "axios": "^0.21.1", "class-validator": "^0.13.1", "cors": "^2.8.5", + "cross-env": "^7.0.3", "decimal.js-light": "^2.5.1", "dotenv": "^10.0.0", "express": "^4.17.1", diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 91f450369..1eee1b9a4 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,11 +10,11 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0033-add_referrer_id', + DB_VERSION: '0035-admin_pending_creations_decimal', DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0 CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v5.2022-04-12', + EXPECTED: 'v6.2022-04-21', CURRENT: '', }, } @@ -50,6 +50,7 @@ const community = { COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung', COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/', COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register', + COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL || 'http://localhost/redeem/{code}', COMMUNITY_DESCRIPTION: process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.', } diff --git a/backend/src/graphql/arg/CreatePendingCreationArgs.ts b/backend/src/graphql/arg/CreatePendingCreationArgs.ts index b90ad3231..0cadf5e62 100644 --- a/backend/src/graphql/arg/CreatePendingCreationArgs.ts +++ b/backend/src/graphql/arg/CreatePendingCreationArgs.ts @@ -1,4 +1,5 @@ -import { ArgsType, Field, Float, InputType, Int } from 'type-graphql' +import { ArgsType, Field, InputType, Int } from 'type-graphql' +import Decimal from 'decimal.js-light' @InputType() @ArgsType() @@ -6,8 +7,8 @@ export default class CreatePendingCreationArgs { @Field(() => String) email: string - @Field(() => Float) - amount: number + @Field(() => Decimal) + amount: Decimal @Field(() => String) memo: string diff --git a/backend/src/graphql/arg/UpdatePendingCreationArgs.ts b/backend/src/graphql/arg/UpdatePendingCreationArgs.ts index 73f70c058..3cd85e84b 100644 --- a/backend/src/graphql/arg/UpdatePendingCreationArgs.ts +++ b/backend/src/graphql/arg/UpdatePendingCreationArgs.ts @@ -1,4 +1,5 @@ -import { ArgsType, Field, Float, Int } from 'type-graphql' +import { ArgsType, Field, Int } from 'type-graphql' +import Decimal from 'decimal.js-light' @ArgsType() export default class UpdatePendingCreationArgs { @@ -8,8 +9,8 @@ export default class UpdatePendingCreationArgs { @Field(() => String) email: string - @Field(() => Float) - amount: number + @Field(() => Decimal) + amount: Decimal @Field(() => String) memo: string diff --git a/backend/src/graphql/directive/isAuthorized.ts b/backend/src/graphql/directive/isAuthorized.ts index 84756c45a..065c01957 100644 --- a/backend/src/graphql/directive/isAuthorized.ts +++ b/backend/src/graphql/directive/isAuthorized.ts @@ -8,7 +8,6 @@ import { RIGHTS } from '@/auth/RIGHTS' import { getCustomRepository } from '@dbTools/typeorm' import { UserRepository } from '@repository/User' import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS' -import { ServerUser } from '@entity/ServerUser' const isAuthorized: AuthChecker = async ({ context }, rights) => { context.role = ROLE_UNAUTHORIZED // unauthorized user @@ -36,8 +35,7 @@ const isAuthorized: AuthChecker = async ({ context }, rights) => { try { const user = await userRepository.findByPubkeyHex(context.pubKey) context.user = user - const countServerUsers = await ServerUser.count({ email: user.email }) - context.role = countServerUsers > 0 ? ROLE_ADMIN : ROLE_USER + context.role = user.isAdmin ? ROLE_ADMIN : ROLE_USER } catch { // in case the database query fails (user deleted) throw new Error('401 Unauthorized') diff --git a/backend/src/graphql/model/PendingCreation.ts b/backend/src/graphql/model/PendingCreation.ts index 594657a59..500ba6f6b 100644 --- a/backend/src/graphql/model/PendingCreation.ts +++ b/backend/src/graphql/model/PendingCreation.ts @@ -1,4 +1,5 @@ import { ObjectType, Field, Int } from 'type-graphql' +import Decimal from 'decimal.js-light' @ObjectType() export class PendingCreation { @@ -23,12 +24,12 @@ export class PendingCreation { @Field(() => String) memo: string - @Field(() => Number) - amount: number + @Field(() => Decimal) + amount: Decimal @Field(() => Number) moderator: number - @Field(() => [Number]) - creation: number[] + @Field(() => [Decimal]) + creation: Decimal[] } diff --git a/backend/src/graphql/model/TransactionLink.ts b/backend/src/graphql/model/TransactionLink.ts index 5081ffd7d..18a601948 100644 --- a/backend/src/graphql/model/TransactionLink.ts +++ b/backend/src/graphql/model/TransactionLink.ts @@ -2,6 +2,7 @@ import { ObjectType, Field, Int } from 'type-graphql' import Decimal from 'decimal.js-light' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' import { User } from './User' +import CONFIG from '@/config' @ObjectType() export class TransactionLink { @@ -17,6 +18,7 @@ export class TransactionLink { this.deletedAt = transactionLink.deletedAt this.redeemedAt = transactionLink.redeemedAt this.redeemedBy = redeemedBy + this.link = CONFIG.COMMUNITY_REDEEM_URL.replace(/{code}/g, this.code) } @Field(() => Number) @@ -51,6 +53,9 @@ export class TransactionLink { @Field(() => User, { nullable: true }) redeemedBy: User | null + + @Field(() => String) + link: string } @ObjectType() diff --git a/backend/src/graphql/model/UpdatePendingCreation.ts b/backend/src/graphql/model/UpdatePendingCreation.ts index c8033f86e..85d3af2cc 100644 --- a/backend/src/graphql/model/UpdatePendingCreation.ts +++ b/backend/src/graphql/model/UpdatePendingCreation.ts @@ -1,4 +1,5 @@ import { ObjectType, Field } from 'type-graphql' +import Decimal from 'decimal.js-light' @ObjectType() export class UpdatePendingCreation { @@ -8,12 +9,12 @@ export class UpdatePendingCreation { @Field(() => String) memo: string - @Field(() => Number) - amount: number + @Field(() => Decimal) + amount: Decimal @Field(() => Number) moderator: number - @Field(() => [Number]) - creation: number[] + @Field(() => [Decimal]) + creation: Decimal[] } diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index 1a187a38f..4f577f60a 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -14,8 +14,8 @@ export class User { this.emailChecked = user.emailChecked this.language = user.language this.publisherId = user.publisherId + this.isAdmin = user.isAdmin // TODO - this.isAdmin = null this.coinanimation = null this.klickTipp = null this.hasElopage = null @@ -58,11 +58,11 @@ export class User { // `passphrase` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, + @Field(() => Date, { nullable: true }) + isAdmin: Date | null + // TODO this is a bit inconsistent with what we query from the database // therefore all those fields are now nullable with default value null - @Field(() => Boolean, { nullable: true }) - isAdmin: boolean | null - @Field(() => Boolean, { nullable: true }) coinanimation: boolean | null diff --git a/backend/src/graphql/model/UserAdmin.ts b/backend/src/graphql/model/UserAdmin.ts index 1d418c66c..8a1459c0f 100644 --- a/backend/src/graphql/model/UserAdmin.ts +++ b/backend/src/graphql/model/UserAdmin.ts @@ -1,9 +1,10 @@ -import { User } from '@entity/User' import { ObjectType, Field, Int } from 'type-graphql' +import Decimal from 'decimal.js-light' +import { User } from '@entity/User' @ObjectType() export class UserAdmin { - constructor(user: User, creation: number[], hasElopage: boolean, emailConfirmationSend: string) { + constructor(user: User, creation: Decimal[], hasElopage: boolean, emailConfirmationSend: string) { this.userId = user.id this.email = user.email this.firstName = user.firstName @@ -27,8 +28,8 @@ export class UserAdmin { @Field(() => String) lastName: string - @Field(() => [Number]) - creation: number[] + @Field(() => [Decimal]) + creation: Decimal[] @Field(() => Boolean) emailChecked: boolean diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 7cc578a87..bac58bbb4 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -43,7 +43,7 @@ import CONFIG from '@/config' // const EMAIL_OPT_IN_REGISTER = 1 // const EMAIL_OPT_UNKNOWN = 3 // elopage? -const MAX_CREATION_AMOUNT = 1000 +const MAX_CREATION_AMOUNT = new Decimal(1000) const FULL_CREATION_AVAILABLE = [MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT] @Resolver() @@ -170,7 +170,7 @@ export class AdminResolver { @Mutation(() => [Number]) async createPendingCreation( @Args() { email, amount, memo, creationDate, moderator }: CreatePendingCreationArgs, - ): Promise { + ): Promise { const user = await dbUser.findOne({ email }, { withDeleted: true }) if (!user) { throw new Error(`Could not find user with email: ${email}`) @@ -186,7 +186,7 @@ export class AdminResolver { if (isCreationValid(creations, amount, creationDateObj)) { const adminPendingCreation = AdminPendingCreation.create() adminPendingCreation.userId = user.id - adminPendingCreation.amount = BigInt(amount) + adminPendingCreation.amount = amount adminPendingCreation.created = new Date() adminPendingCreation.date = creationDateObj adminPendingCreation.memo = memo @@ -251,14 +251,14 @@ export class AdminResolver { if (!isCreationValid(creations, amount, creationDateObj)) { throw new Error('Creation is not valid') } - pendingCreationToUpdate.amount = BigInt(amount) + pendingCreationToUpdate.amount = amount pendingCreationToUpdate.memo = memo pendingCreationToUpdate.date = new Date(creationDate) pendingCreationToUpdate.moderator = moderator await AdminPendingCreation.save(pendingCreationToUpdate) const result = new UpdatePendingCreation() - result.amount = parseInt(amount.toString()) + result.amount = amount result.memo = pendingCreationToUpdate.memo result.date = pendingCreationToUpdate.date result.moderator = pendingCreationToUpdate.moderator @@ -286,7 +286,7 @@ export class AdminResolver { return { ...pendingCreation, - amount: Number(pendingCreation.amount.toString()), + amount: pendingCreation.amount, firstName: user ? user.firstName : '', lastName: user ? user.lastName : '', email: user ? user.email : '', @@ -318,7 +318,7 @@ export class AdminResolver { if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a creation.') const creations = await getUserCreation(pendingCreation.userId, false) - if (!isCreationValid(creations, Number(pendingCreation.amount), pendingCreation.date)) { + if (!isCreationValid(creations, pendingCreation.amount, pendingCreation.date)) { throw new Error('Creation is not valid!!') } @@ -333,8 +333,7 @@ export class AdminResolver { decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, receivedCallDate) newBalance = decay.balance } - // TODO pending creations decimal - newBalance = newBalance.add(new Decimal(Number(pendingCreation.amount)).toString()) + newBalance = newBalance.add(pendingCreation.amount.toString()) const transaction = new DbTransaction() transaction.typeId = TransactionTypeId.CREATION @@ -448,10 +447,10 @@ export class AdminResolver { interface CreationMap { id: number - creations: number[] + creations: Decimal[] } -async function getUserCreation(id: number, includePending = true): Promise { +async function getUserCreation(id: number, includePending = true): Promise { const creations = await getUserCreations([id], includePending) return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE } @@ -493,30 +492,30 @@ async function getUserCreations(ids: number[], includePending = true): Promise parseInt(raw.month) === month && parseInt(raw.id) === id, ) - return MAX_CREATION_AMOUNT - (creation ? Number(creation.sum) : 0) + return MAX_CREATION_AMOUNT.minus(creation ? creation.sum : 0) }), } }) } -function updateCreations(creations: number[], pendingCreation: AdminPendingCreation): number[] { +function updateCreations(creations: Decimal[], pendingCreation: AdminPendingCreation): Decimal[] { const index = getCreationIndex(pendingCreation.date.getMonth()) if (index < 0) { throw new Error('You cannot create GDD for a month older than the last three months.') } - creations[index] += parseInt(pendingCreation.amount.toString()) + creations[index] = creations[index].plus(pendingCreation.amount) return creations } -function isCreationValid(creations: number[], amount: number, creationDate: Date) { +function isCreationValid(creations: Decimal[], amount: Decimal, creationDate: Date) { const index = getCreationIndex(creationDate.getMonth()) if (index < 0) { throw new Error(`No Creation found!`) } - if (amount > creations[index]) { + if (amount.greaterThan(creations[index].toString())) { throw new Error( `The amount (${amount} GDD) to be created exceeds the available amount (${creations[index]} GDD) for this month.`, ) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 07b8e59e2..c658476a4 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -1,17 +1,18 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers' +import { testEnvironment, headerPushMock, resetToken, cleanDB, resetEntity } from '@test/helpers' import { userFactory } from '@/seeds/factory/user' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' -import { createUser, setPassword } from '@/seeds/graphql/mutations' -import { login, logout } from '@/seeds/graphql/queries' +import { createUser, setPassword, forgotPassword, updateUserInfos } from '@/seeds/graphql/mutations' +import { login, logout, verifyLogin, queryOptIn } from '@/seeds/graphql/queries' import { GraphQLError } from 'graphql' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { User } from '@entity/User' import CONFIG from '@/config' import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' -import { printTimeDuration } from './UserResolver' +import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail' +import { printTimeDuration, activationLink } from './UserResolver' // import { klicktippSignIn } from '@/apis/KlicktippController' @@ -22,6 +23,13 @@ jest.mock('@/mailer/sendAccountActivationEmail', () => { } }) +jest.mock('@/mailer/sendResetPasswordEmail', () => { + return { + __esModule: true, + sendResetPasswordEmail: jest.fn(), + } +}) + /* jest.mock('@/apis/KlicktippController', () => { return { @@ -85,7 +93,7 @@ describe('UserResolver', () => { }) describe('filling all tables', () => { - it('saves the user in login_user table', () => { + it('saves the user in users table', () => { expect(user).toEqual([ { id: expect.any(Number), @@ -100,6 +108,7 @@ describe('UserResolver', () => { emailChecked: false, passphrase: expect.any(String), language: 'de', + isAdmin: null, deletedAt: null, publisherId: 1234, referrerId: null, @@ -336,7 +345,7 @@ describe('UserResolver', () => { firstName: 'Bibi', hasElopage: false, id: expect.any(Number), - isAdmin: false, + isAdmin: null, klickTipp: { newsletterState: false, }, @@ -412,6 +421,356 @@ describe('UserResolver', () => { }) }) }) + + describe('verifyLogin', () => { + describe('unauthenticated', () => { + it('throws an error', async () => { + resetToken() + await expect(query({ query: verifyLogin })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('user exists but is not logged in', () => { + beforeAll(async () => { + await userFactory(testEnv, bibiBloxberg) + }) + + afterAll(async () => { + await cleanDB() + }) + + it('throws an error', async () => { + resetToken() + await expect(query({ query: verifyLogin })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + + describe('authenticated', () => { + const variables = { + email: 'bibi@bloxberg.de', + password: 'Aa12345_', + } + + beforeAll(async () => { + await query({ query: login, variables }) + }) + + afterAll(() => { + resetToken() + }) + + it('returns user object', async () => { + await expect(query({ query: verifyLogin })).resolves.toEqual( + expect.objectContaining({ + data: { + verifyLogin: { + email: 'bibi@bloxberg.de', + firstName: 'Bibi', + lastName: 'Bloxberg', + language: 'de', + coinanimation: true, + klickTipp: { + newsletterState: false, + }, + hasElopage: false, + publisherId: 1234, + isAdmin: null, + }, + }, + }), + ) + }) + }) + }) + }) + + describe('forgotPassword', () => { + const variables = { email: 'bibi@bloxberg.de' } + describe('user is not in DB', () => { + it('returns true', async () => { + await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual( + expect.objectContaining({ + data: { + forgotPassword: true, + }, + }), + ) + }) + }) + + describe('user exists in DB', () => { + let result: any + let loginEmailOptIn: LoginEmailOptIn[] + + beforeAll(async () => { + await userFactory(testEnv, bibiBloxberg) + await resetEntity(LoginEmailOptIn) + result = await mutate({ mutation: forgotPassword, variables }) + loginEmailOptIn = await LoginEmailOptIn.find() + }) + + afterAll(async () => { + await cleanDB() + }) + + it('returns true', async () => { + await expect(result).toEqual( + expect.objectContaining({ + data: { + forgotPassword: true, + }, + }), + ) + }) + + it('sends reset password email', () => { + expect(sendResetPasswordEmail).toBeCalledWith({ + link: activationLink(loginEmailOptIn[0]), + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + duration: expect.any(String), + }) + }) + + describe('request reset password again', () => { + it('thows an error', async () => { + await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')], + }), + ) + }) + }) + }) + }) + + describe('queryOptIn', () => { + let loginEmailOptIn: LoginEmailOptIn[] + + beforeAll(async () => { + await userFactory(testEnv, bibiBloxberg) + loginEmailOptIn = await LoginEmailOptIn.find() + }) + + afterAll(async () => { + await cleanDB() + }) + + describe('wrong optin code', () => { + it('throws an error', async () => { + await expect( + query({ query: queryOptIn, variables: { optIn: 'not-valid' } }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + // keep Whitspace in error message! + new GraphQLError(`Could not find any entity of type "LoginEmailOptIn" matching: { + "verificationCode": "not-valid" +}`), + ], + }), + ) + }) + }) + + describe('correct optin code', () => { + it('returns true', async () => { + await expect( + query({ + query: queryOptIn, + variables: { optIn: loginEmailOptIn[0].verificationCode.toString() }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + queryOptIn: true, + }, + }), + ) + }) + }) + }) + + describe('updateUserInfos', () => { + describe('unauthenticated', () => { + it('throws an error', async () => { + resetToken() + await expect(mutate({ mutation: updateUserInfos })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + await userFactory(testEnv, bibiBloxberg) + await query({ + query: login, + variables: { + email: 'bibi@bloxberg.de', + password: 'Aa12345_', + }, + }) + }) + + afterAll(async () => { + await cleanDB() + }) + + it('returns true', async () => { + await expect(mutate({ mutation: updateUserInfos })).resolves.toEqual( + expect.objectContaining({ + data: { + updateUserInfos: true, + }, + }), + ) + }) + + describe('first-name, last-name and language', () => { + it('updates the fields in DB', async () => { + await mutate({ + mutation: updateUserInfos, + variables: { + firstName: 'Benjamin', + lastName: 'BlĂŒmchen', + locale: 'en', + }, + }) + await expect(User.findOne()).resolves.toEqual( + expect.objectContaining({ + firstName: 'Benjamin', + lastName: 'BlĂŒmchen', + language: 'en', + }), + ) + }) + }) + + describe('language is not valid', () => { + it('thows an error', async () => { + await expect( + mutate({ + mutation: updateUserInfos, + variables: { + locale: 'not-valid', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError(`"not-valid" isn't a valid language`)], + }), + ) + }) + }) + + describe('password', () => { + describe('wrong old password', () => { + it('throws an error', async () => { + await expect( + mutate({ + mutation: updateUserInfos, + variables: { + password: 'wrong password', + passwordNew: 'Aa12345_', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Old password is invalid')], + }), + ) + }) + }) + + describe('invalid new password', () => { + it('throws an error', async () => { + await expect( + mutate({ + mutation: updateUserInfos, + variables: { + password: 'Aa12345_', + passwordNew: 'Aa12345', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + new GraphQLError( + 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!', + ), + ], + }), + ) + }) + }) + + describe('correct old and new password', () => { + it('returns true', async () => { + await expect( + mutate({ + mutation: updateUserInfos, + variables: { + password: 'Aa12345_', + passwordNew: 'Bb12345_', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { updateUserInfos: true }, + }), + ) + }) + + it('can login wtih new password', async () => { + await expect( + query({ + query: login, + variables: { + email: 'bibi@bloxberg.de', + password: 'Bb12345_', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + login: expect.objectContaining({ + email: 'bibi@bloxberg.de', + }), + }, + }), + ) + }) + + it('cannot login wtih old password', async () => { + await expect( + query({ + query: login, + variables: { + email: 'bibi@bloxberg.de', + password: 'Aa12345_', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('No user with this credentials')], + }), + ) + }) + }) + }) + }) + }) }) describe('printTimeDuration', () => { diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 137c09622..4ab5a901b 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -19,9 +19,7 @@ import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' import { klicktippSignIn } from '@/apis/KlicktippController' import { RIGHTS } from '@/auth/RIGHTS' -import { ROLE_ADMIN } from '@/auth/ROLES' import { hasElopageBuys } from '@/util/hasElopageBuys' -import { ServerUser } from '@entity/ServerUser' // eslint-disable-next-line @typescript-eslint/no-var-requires const sodium = require('sodium-native') @@ -207,7 +205,6 @@ export class UserResolver { }) user.coinanimation = coinanimation - user.isAdmin = context.role === ROLE_ADMIN return user } @@ -243,16 +240,11 @@ export class UserResolver { } const user = new User(dbUser) - // user.email = email - // user.pubkey = dbUser.pubKey.toString('hex') - user.language = dbUser.language // Elopage Status & Stored PublisherId user.hasElopage = await this.hasElopage({ ...context, user: dbUser }) if (!user.hasElopage && publisherId) { user.publisherId = publisherId - // TODO: Check if we can use updateUserInfos - // await this.updateUserInfos({ publisherId }, { pubKey: loginUser.pubKey }) dbUser.publisherId = publisherId DbUser.save(dbUser) } @@ -266,10 +258,6 @@ export class UserResolver { }) user.coinanimation = coinanimation - // context.role is not set to the actual role yet on login - const countServerUsers = await ServerUser.count({ email: user.email }) - user.isAdmin = countServerUsers > 0 - context.setHeaders.push({ key: 'token', value: encode(dbUser.pubKey), @@ -529,15 +517,7 @@ export class UserResolver { @Mutation(() => Boolean) async updateUserInfos( @Args() - { - firstName, - lastName, - language, - publisherId, - password, - passwordNew, - coinanimation, - }: UpdateUserInfosArgs, + { firstName, lastName, language, password, passwordNew, coinanimation }: UpdateUserInfosArgs, @Ctx() context: Context, ): Promise { const userEntity = getUser(context) @@ -581,11 +561,6 @@ export class UserResolver { userEntity.privKey = encryptedPrivkey } - // Save publisherId only if Elopage is not yet registered - if (publisherId && !(await this.hasElopage(context))) { - userEntity.publisherId = publisherId - } - const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() await queryRunner.startTransaction('READ UNCOMMITTED') diff --git a/backend/src/seeds/factory/user.ts b/backend/src/seeds/factory/user.ts index ff4c1d6c9..4b5913d48 100644 --- a/backend/src/seeds/factory/user.ts +++ b/backend/src/seeds/factory/user.ts @@ -1,7 +1,6 @@ import { createUser, setPassword } from '@/seeds/graphql/mutations' import { User } from '@entity/User' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' -import { ServerUser } from '@entity/ServerUser' import { UserInterface } from '@/seeds/users/UserInterface' import { ApolloServerTestClient } from 'apollo-server-testing' @@ -29,23 +28,9 @@ export const userFactory = async ( // get user from database const dbUser = await User.findOneOrFail({ id }) - if (user.createdAt || user.deletedAt) { - if (user.createdAt) dbUser.createdAt = user.createdAt - if (user.deletedAt) dbUser.deletedAt = user.deletedAt - await dbUser.save() - } - - if (user.isAdmin) { - const admin = new ServerUser() - admin.username = dbUser.firstName - admin.password = 'please_refactor' - admin.email = dbUser.email - admin.role = 'admin' - admin.activated = 1 - admin.lastLogin = new Date() - admin.created = dbUser.createdAt - admin.modified = dbUser.createdAt - await admin.save() - } + if (user.createdAt) dbUser.createdAt = user.createdAt + if (user.deletedAt) dbUser.deletedAt = user.deletedAt + if (user.isAdmin) dbUser.isAdmin = new Date() + await dbUser.save() } } diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index 298d56bdb..601b1fbbf 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -18,6 +18,12 @@ export const setPassword = gql` } ` +export const forgotPassword = gql` + mutation ($email: String!) { + forgotPassword(email: $email) + } +` + export const updateUserInfos = gql` mutation ( $firstName: String @@ -80,7 +86,7 @@ export const createTransactionLink = gql` export const createPendingCreation = gql` mutation ( $email: String! - $amount: Float! + $amount: Decimal! $memo: String! $creationDate: String! $moderator: Int! diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 11a675eeb..76a386953 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -43,6 +43,12 @@ export const logout = gql` } ` +export const queryOptIn = gql` + query ($optIn: String!) { + queryOptIn(optIn: $optIn) + } +` + export const transactionsQuery = gql` query ( $currentPage: Int = 1 diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index 33ac2fad2..0d0d12f6c 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -17,6 +17,7 @@ const communityDbUser: dbUser = { createdAt: new Date(), emailChecked: false, language: '', + isAdmin: null, publisherId: 0, passphrase: '', settings: [], diff --git a/backend/src/util/virtualTransactions.ts b/backend/src/util/virtualTransactions.ts index 8c1aec65b..08d44b48d 100644 --- a/backend/src/util/virtualTransactions.ts +++ b/backend/src/util/virtualTransactions.ts @@ -70,6 +70,7 @@ const virtualDecayTransaction = ( typeId: TransactionTypeId.DECAY, amount: decay.decay ? decay.roundedDecay : new Decimal(0), balance: decay.balance + .toDecimalPlaces(2, Decimal.ROUND_DOWN) .minus(holdAvailabeAmount.toString()) .toDecimalPlaces(2, Decimal.ROUND_DOWN), balanceDate: time, diff --git a/backend/yarn.lock b/backend/yarn.lock index 3ba20211a..f37b64d11 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1900,7 +1900,14 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + +cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== diff --git a/database/README.md b/database/README.md index 84db3d194..d6cf84518 100644 --- a/database/README.md +++ b/database/README.md @@ -30,4 +30,3 @@ yarn dev_down yarn dev_reset ``` Runs all down migrations and after this all up migrations. - diff --git a/database/entity/0034-drop_server_user_table/User.ts b/database/entity/0034-drop_server_user_table/User.ts new file mode 100644 index 000000000..1f56d13d2 --- /dev/null +++ b/database/entity/0034-drop_server_user_table/User.ts @@ -0,0 +1,81 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + OneToMany, + DeleteDateColumn, +} from 'typeorm' +import { UserSetting } from '../UserSetting' + +@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) +export class User extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true }) + pubKey: Buffer + + @Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true }) + privKey: Buffer + + @Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' }) + email: string + + @Column({ + name: 'first_name', + length: 255, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + firstName: string + + @Column({ + name: 'last_name', + length: 255, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + lastName: string + + @DeleteDateColumn() + deletedAt: Date | null + + @Column({ type: 'bigint', default: 0, unsigned: true }) + password: BigInt + + @Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true }) + emailHash: Buffer + + @Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false }) + createdAt: Date + + @Column({ name: 'email_checked', type: 'bool', nullable: false, default: false }) + emailChecked: boolean + + @Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false }) + language: string + + @Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null }) + isAdmin: Date | null + + @Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null }) + referrerId?: number | null + + @Column({ name: 'publisher_id', default: 0 }) + publisherId: number + + @Column({ + type: 'text', + name: 'passphrase', + collation: 'utf8mb4_unicode_ci', + nullable: true, + default: null, + }) + passphrase: string + + @OneToMany(() => UserSetting, (userSetting) => userSetting.user) + settings: UserSetting[] +} diff --git a/database/entity/0035-admin_pending_creations_decimal/AdminPendingCreation.ts b/database/entity/0035-admin_pending_creations_decimal/AdminPendingCreation.ts new file mode 100644 index 000000000..3cd83a3a5 --- /dev/null +++ b/database/entity/0035-admin_pending_creations_decimal/AdminPendingCreation.ts @@ -0,0 +1,33 @@ +import Decimal from 'decimal.js-light' +import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm' +import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' + +@Entity('admin_pending_creations') +export class AdminPendingCreation extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ unsigned: true, nullable: false }) + userId: number + + @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) + created: Date + + @Column({ type: 'datetime', nullable: false }) + date: Date + + @Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' }) + memo: string + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: false, + transformer: DecimalTransformer, + }) + amount: Decimal + + @Column() + moderator: number +} diff --git a/database/entity/AdminPendingCreation.ts b/database/entity/AdminPendingCreation.ts index 03eeab883..b2b37d7c4 100644 --- a/database/entity/AdminPendingCreation.ts +++ b/database/entity/AdminPendingCreation.ts @@ -1 +1 @@ -export { AdminPendingCreation } from './0015-admin_pending_creations/AdminPendingCreation' +export { AdminPendingCreation } from './0035-admin_pending_creations_decimal/AdminPendingCreation' diff --git a/database/entity/ServerUser.ts b/database/entity/ServerUser.ts deleted file mode 100644 index 495513823..000000000 --- a/database/entity/ServerUser.ts +++ /dev/null @@ -1 +0,0 @@ -export { ServerUser } from './0001-init_db/ServerUser' diff --git a/database/entity/User.ts b/database/entity/User.ts index 35dfb7bbe..4cd68174c 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0033-add_referrer_id/User' +export { User } from './0034-drop_server_user_table/User' diff --git a/database/entity/index.ts b/database/entity/index.ts index cb6f56ab0..542333755 100644 --- a/database/entity/index.ts +++ b/database/entity/index.ts @@ -1,7 +1,6 @@ import { LoginElopageBuys } from './LoginElopageBuys' import { LoginEmailOptIn } from './LoginEmailOptIn' import { Migration } from './Migration' -import { ServerUser } from './ServerUser' import { Transaction } from './Transaction' import { TransactionLink } from './TransactionLink' import { User } from './User' @@ -13,7 +12,6 @@ export const entities = [ LoginElopageBuys, LoginEmailOptIn, Migration, - ServerUser, Transaction, TransactionLink, User, diff --git a/database/migrations/0034-drop_server_user_table.ts b/database/migrations/0034-drop_server_user_table.ts new file mode 100644 index 000000000..be6b44489 --- /dev/null +++ b/database/migrations/0034-drop_server_user_table.ts @@ -0,0 +1,37 @@ +/* MIGRATION DROP server_users TABLE +add isAdmin COLUMN to users TABLE */ + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('ALTER TABLE `users` ADD COLUMN `is_admin` datetime DEFAULT NULL AFTER `language`;') + + await queryFn( + 'UPDATE users AS users INNER JOIN server_users AS server_users ON users.email = server_users.email SET users.is_admin = server_users.modified WHERE users.email IN (SELECT email from server_users);', + ) + + await queryFn('DROP TABLE `server_users`;') +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn(` + CREATE TABLE IF NOT EXISTS \`server_users\` ( + \`id\` int(10) unsigned NOT NULL AUTO_INCREMENT, + \`username\` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + \`password\` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + \`email\` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + \`role\` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'admin', + \`activated\` tinyint(4) NOT NULL DEFAULT '0', + \`last_login\` datetime DEFAULT NULL, + \`created\` datetime NOT NULL, + \`modified\` datetime NOT NULL, + PRIMARY KEY (\`id\`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`) + + await queryFn( + 'INSERT INTO `server_users` (`email`, `username`, `password`, `created`, `modified`) SELECT `email`, `first_name`, `password`, `is_admin`, `is_admin` FROM `users` WHERE `is_admin` IS NOT NULL;', + ) + + await queryFn('ALTER TABLE `users` DROP COLUMN `is_admin`;') +} diff --git a/database/migrations/0035-admin_pending_creations_decimal.ts b/database/migrations/0035-admin_pending_creations_decimal.ts new file mode 100644 index 000000000..d3648f376 --- /dev/null +++ b/database/migrations/0035-admin_pending_creations_decimal.ts @@ -0,0 +1,42 @@ +/* MIGRATION TO CHANGE SEVERAL FIELDS ON `admin_pending_creations` + * - `amount` FIELD TYPE TO `Decimal` + * - `memo` FIELD TYPE TO `varchar(255)`, collate `utf8mb4_unicode_ci` + */ + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + // rename `amount` to `amount_bigint` + await queryFn('ALTER TABLE `admin_pending_creations` RENAME COLUMN `amount` TO `amount_bigint`;') + // add `amount` (decimal) + await queryFn( + 'ALTER TABLE `admin_pending_creations` ADD COLUMN `amount` DECIMAL(40,20) DEFAULT NULL AFTER `amount_bigint`;', + ) + // fill new `amount` column + await queryFn('UPDATE `admin_pending_creations` SET `amount` = `amount_bigint` DIV 10000;') + // make `amount` not nullable + await queryFn( + 'ALTER TABLE `admin_pending_creations` MODIFY COLUMN `amount` DECIMAL(40,20) NOT NULL;', + ) + // drop `amount_bitint` column + await queryFn('ALTER TABLE `admin_pending_creations` DROP COLUMN `amount_bigint`;') + + // change `memo` to varchar(255), collate utf8mb4_unicode_ci + await queryFn( + 'ALTER TABLE `admin_pending_creations` MODIFY COLUMN `memo` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL;', + ) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('ALTER TABLE `admin_pending_creations` MODIFY COLUMN `memo` text DEFAULT NULL;') + await queryFn( + 'ALTER TABLE `admin_pending_creations` ADD COLUMN `amount_bigint` bigint(20) DEFAULT NULL AFTER `amount`;', + ) + await queryFn('UPDATE `admin_pending_creations` SET `amount_bigint` = `amount` * 10000;') + await queryFn( + 'ALTER TABLE `admin_pending_creations` MODIFY COLUMN `amount_bigint` bigint(20) NOT NULL;', + ) + await queryFn('ALTER TABLE `admin_pending_creations` DROP COLUMN `amount`;') + await queryFn('ALTER TABLE `admin_pending_creations` RENAME COLUMN `amount_bigint` TO `amount`;') +} diff --git a/database/package.json b/database/package.json index 40c43c9b5..13c638c79 100644 --- a/database/package.json +++ b/database/package.json @@ -1,6 +1,6 @@ { "name": "gradido-database", - "version": "1.7.1", + "version": "1.8.0", "description": "Gradido Database Tool to execute database migrations", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/database", @@ -10,15 +10,15 @@ "scripts": { "build": "mkdir -p build/src/config/ && cp src/config/*.txt build/src/config/ && tsc --build", "clean": "tsc --build --clean", - "up": "TZ=UTC node build/src/index.js up", - "down": "TZ=UTC node build/src/index.js down", - "reset": "TZ=UTC node build/src/index.js reset", - "dev_up": "TZ=UTC ts-node src/index.ts up", - "dev_down": "TZ=UTC ts-node src/index.ts down", - "dev_reset": "TZ=UTC ts-node src/index.ts reset", + "up": "cross-env TZ=UTC node build/src/index.js up", + "down": "cross-env TZ=UTC node build/src/index.js down", + "reset": "cross-env TZ=UTC node build/src/index.js reset", + "dev_up": "cross-env TZ=UTC ts-node src/index.ts up", + "dev_down": "cross-env TZ=UTC ts-node src/index.ts down", + "dev_reset": "cross-env TZ=UTC ts-node src/index.ts reset", "lint": "eslint --max-warnings=0 --ext .js,.ts .", "seed:config": "ts-node ./node_modules/typeorm-seeding/dist/cli.js config", - "seed": "TZ=UTC ts-node src/index.ts seed" + "seed": "cross-env TZ=UTC ts-node src/index.ts seed" }, "devDependencies": { "@types/faker": "^5.5.9", @@ -37,6 +37,7 @@ "typescript": "^4.3.5" }, "dependencies": { + "cross-env": "^7.0.3", "crypto": "^1.0.1", "decimal.js-light": "^2.5.1", "dotenv": "^10.0.0", diff --git a/database/yarn.lock b/database/yarn.lock index a2df693ec..e5d74929c 100644 --- a/database/yarn.lock +++ b/database/yarn.lock @@ -481,7 +481,14 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-spawn@^7.0.2: +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + +cross-spawn@^7.0.1, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index efac0fc68..a7e266bdf 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -21,10 +21,11 @@ WEBHOOK_GITHUB_BRANCH=master COMMUNITY_NAME="Gradido Development Stage1" COMMUNITY_URL=https://stage1.gradido.net/ COMMUNITY_REGISTER_URL=https://stage1.gradido.net/register +COMMUNITY_REDEEM_URL=https://stage1.gradido.net/redeem/{code} COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community" # backend -BACKEND_CONFIG_VERSION=v5.2022-04-12 +BACKEND_CONFIG_VERSION=v6.2022-04-21 JWT_EXPIRES_IN=30m GDT_API_URL=https://gdt.gradido.net @@ -37,7 +38,7 @@ KLICKTIPP_PASSWORD= KLICKTIPP_APIKEY_DE= KLICKTIPP_APIKEY_EN= -EMAIL=true +EMAIL=false EMAIL_USERNAME=peter@lustig.de EMAIL_SENDER=peter@lustig.de EMAIL_PASSWORD=1234 diff --git a/docu/graphics/federation.drawio b/docu/graphics/federation.drawio new file mode 100644 index 000000000..1b4db9002 --- /dev/null +++ b/docu/graphics/federation.drawio @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docu/graphics/federation.png b/docu/graphics/federation.png new file mode 100644 index 000000000..6d25708f8 Binary files /dev/null and b/docu/graphics/federation.png differ diff --git a/frontend/package.json b/frontend/package.json index 3bd975798..18021e705 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "bootstrap-vue-gradido-wallet", - "version": "1.7.1", + "version": "1.8.0", "private": true, "scripts": { "start": "node run/server.js", @@ -10,7 +10,7 @@ "analyse-bundle": "yarn build && webpack-bundle-analyzer dist/webpack.stats.json", "lint": "eslint --max-warnings=0 --ext .js,.vue,.json .", "stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'", - "test": "TZ=UTC jest --coverage", + "test": "cross-env TZ=UTC jest --coverage", "locales": "scripts/sort.sh" }, "dependencies": { @@ -72,6 +72,7 @@ "babel-eslint": "^10.1.0", "babel-plugin-component": "^1.1.0", "babel-plugin-transform-require-context": "^0.1.1", + "cross-env": "^7.0.3", "dotenv-webpack": "^7.0.3", "postcss": "^8.4.8", "postcss-html": "^1.3.0", diff --git a/frontend/src/components/ClipboardCopy.vue b/frontend/src/components/ClipboardCopy.vue index 810f73fe1..7a6cf0ec1 100644 --- a/frontend/src/components/ClipboardCopy.vue +++ b/frontend/src/components/ClipboardCopy.vue @@ -1,32 +1,50 @@ + diff --git a/frontend/src/components/DecayInformations/CollapseLinksList.spec.js b/frontend/src/components/DecayInformations/CollapseLinksList.spec.js index 4fc527e39..68d5d427f 100644 --- a/frontend/src/components/DecayInformations/CollapseLinksList.spec.js +++ b/frontend/src/components/DecayInformations/CollapseLinksList.spec.js @@ -17,6 +17,7 @@ const propsData = { { amount: '5', code: 'ce28664b5308c17f931c0367', + link: 'http://localhost/redeem/ce28664b5308c17f931c0367', createdAt: '2022-03-16T14:22:40.000Z', holdAvailableAmount: '5.13109484759482747111', id: 87, @@ -27,6 +28,7 @@ const propsData = { { amount: '6', code: 'ce28664b5308c17f931c0367', + link: 'http://localhost/redeem/ce28664b5308c17f931c0367', createdAt: '2022-03-16T14:22:40.000Z', holdAvailableAmount: '5.13109484759482747111', id: 86, diff --git a/frontend/src/components/GddSend/TransactionResultLink.vue b/frontend/src/components/GddSend/TransactionResultLink.vue index ee65b159a..04445acfe 100644 --- a/frontend/src/components/GddSend/TransactionResultLink.vue +++ b/frontend/src/components/GddSend/TransactionResultLink.vue @@ -3,13 +3,12 @@
{{ $t('gdd_per_link.created') }}
- - +
- + - + {{ $t('form.close') }}
@@ -28,14 +27,19 @@ export default { FigureQrCode, }, props: { - code: { + link: { type: String, required: true, }, }, - computed: { - link() { - return `${window.location.origin}/redeem/${this.code}` + data() { + return { + showQrcode: false, + } + }, + methods: { + showQrCodeButton() { + this.showQrcode = !this.showQrcode }, }, } diff --git a/frontend/src/components/GddSend/TransactionResultSendError.vue b/frontend/src/components/GddSend/TransactionResultSendError.vue index 6a3761092..279ee931e 100644 --- a/frontend/src/components/GddSend/TransactionResultSendError.vue +++ b/frontend/src/components/GddSend/TransactionResultSendError.vue @@ -22,7 +22,9 @@
{{ errorResult }}

- {{ $t('form.close') }} + + {{ $t('form.close') }} +

diff --git a/frontend/src/components/GddSend/TransactionResultSendSuccess.vue b/frontend/src/components/GddSend/TransactionResultSendSuccess.vue index 74bb6963a..bc8c39f3b 100644 --- a/frontend/src/components/GddSend/TransactionResultSendSuccess.vue +++ b/frontend/src/components/GddSend/TransactionResultSendSuccess.vue @@ -9,7 +9,7 @@ {{ $t('form.send_transaction_success') }}

- {{ $t('form.close') }} + {{ $t('form.close') }}

diff --git a/frontend/src/components/QrCode/FigureQrCode.spec.js b/frontend/src/components/QrCode/FigureQrCode.spec.js index d19d806d3..715a5d5d5 100644 --- a/frontend/src/components/QrCode/FigureQrCode.spec.js +++ b/frontend/src/components/QrCode/FigureQrCode.spec.js @@ -4,7 +4,7 @@ import FigureQrCode from './FigureQrCode' const localVue = global.localVue const propsData = { - text: '', + link: '', } describe('FigureQrCode', () => { diff --git a/frontend/src/components/QrCode/FigureQrCode.vue b/frontend/src/components/QrCode/FigureQrCode.vue index df450a52e..00f1b54b9 100644 --- a/frontend/src/components/QrCode/FigureQrCode.vue +++ b/frontend/src/components/QrCode/FigureQrCode.vue @@ -14,14 +14,14 @@ export default { QRCanvas, }, props: { - text: { type: String, required: true }, + link: { type: String, required: true }, }, data() { return { options: { cellSize: 8, correctLevel: 'H', - data: this.text, + data: this.link, }, } }, diff --git a/frontend/src/components/TransactionLinks/TransactionLink.spec.js b/frontend/src/components/TransactionLinks/TransactionLink.spec.js index ec2f657c3..ad9e4860e 100644 --- a/frontend/src/components/TransactionLinks/TransactionLink.spec.js +++ b/frontend/src/components/TransactionLinks/TransactionLink.spec.js @@ -22,7 +22,7 @@ const mocks = { const propsData = { amount: '75', - code: 'c00000000c000000c0000', + link: 'http://localhost/redeem/c00000000c000000c0000', holdAvailableAmount: '5.13109484759482747111', id: 12, memo: 'Katzenauge, Eulenschrei, was verschwunden komm herbei!', @@ -44,115 +44,140 @@ describe('TransactionLink', () => { expect(wrapper.find('div.transaction-link').exists()).toBeTruthy() }) - describe('Copy link to Clipboard', () => { - const navigatorClipboard = navigator.clipboard - beforeAll(() => { - delete navigator.clipboard - navigator.clipboard = { writeText: navigatorClipboardMock } - }) - afterAll(() => { - navigator.clipboard = navigatorClipboard + describe('Link validUntil Date is not valid', () => { + it('has no copy link button', () => { + expect(wrapper.find('.test-copy-link').exists()).toBe(false) }) - describe('copy with success', () => { - beforeEach(async () => { - navigatorClipboardMock.mockResolvedValue() - await wrapper.find('.test-copy-link').trigger('click') - }) - it('should call clipboard.writeText', () => { - expect(navigator.clipboard.writeText).toHaveBeenCalledWith( - 'http://localhost/redeem/c00000000c000000c0000', - ) - }) - it('toasts success message', () => { - expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.link-copied') - }) + it('has no Qr-Code Button ', () => { + expect(wrapper.find('.test-qr-code').exists()).toBe(false) + }) + + it('has delete link button ', () => { + expect(wrapper.find('.test-delete-link').exists()).toBe(true) }) }) - describe('qr code modal', () => { - let spy - - beforeEach(() => { + describe('Link validUntil Date is valid ', () => { + beforeEach(async () => { + const now = new Date() jest.clearAllMocks() + await wrapper.setProps({ + validUntil: `${new Date(now.getFullYear(), now.getMonth(), now.getDate() + 2)}`, + }) }) - describe('with success', () => { + describe('Copy link to Clipboard', () => { + const navigatorClipboard = navigator.clipboard + beforeAll(() => { + delete navigator.clipboard + navigator.clipboard = { writeText: navigatorClipboardMock } + }) + afterAll(() => { + navigator.clipboard = navigatorClipboard + }) + + describe('copy with success', () => { + beforeEach(async () => { + navigatorClipboardMock.mockResolvedValue() + await wrapper.find('.test-copy-link .dropdown-item').trigger('click') + }) + + it('should call clipboard.writeText', () => { + expect(navigator.clipboard.writeText).toHaveBeenCalledWith( + 'http://localhost/redeem/c00000000c000000c0000', + ) + }) + it('toasts success message', () => { + expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.link-copied') + }) + }) + }) + + describe('qr code modal', () => { + let spy + beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'show') - // spy.mockImplementation(() => Promise.resolve('some value')) - // mockAPIcall.mockResolvedValue() - await wrapper.find('.test-qr-code').trigger('click') + jest.clearAllMocks() }) - it('qr-code Modal if show', () => { - expect(spy).toBeCalled() + describe('with success', () => { + beforeEach(async () => { + spy = jest.spyOn(wrapper.vm.$bvModal, 'show') + // spy.mockImplementation(() => Promise.resolve('some value')) + // mockAPIcall.mockResolvedValue() + await wrapper.find('.test-qr-code .dropdown-item').trigger('click') + }) + + it('opens the qr-code Modal', () => { + expect(spy).toBeCalled() + }) }) }) - }) - describe('delete link', () => { - let spy + describe('delete link', () => { + let spy - beforeEach(() => { - jest.clearAllMocks() - }) - - describe('with success', () => { beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - spy.mockImplementation(() => Promise.resolve('some value')) - mockAPIcall.mockResolvedValue() - await wrapper.find('.test-delete-link').trigger('click') + jest.clearAllMocks() }) - it('test Modal if confirm true', () => { - expect(spy).toBeCalled() + describe('with success', () => { + beforeEach(async () => { + spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') + spy.mockImplementation(() => Promise.resolve('some value')) + mockAPIcall.mockResolvedValue() + await wrapper.find('.test-delete-link .dropdown-item').trigger('click') + }) + + it('opens the modal ', () => { + expect(spy).toBeCalled() + }) + + it('calls the API', () => { + expect(mockAPIcall).toBeCalledWith( + expect.objectContaining({ + mutation: deleteTransactionLink, + variables: { + id: 12, + }, + }), + ) + }) + + it('toasts a success message', () => { + expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.deleted') + }) + + it('emits reset transaction link list', () => { + expect(wrapper.emitted('reset-transaction-link-list')).toBeTruthy() + }) }) - it('calls the API', () => { - expect(mockAPIcall).toBeCalledWith( - expect.objectContaining({ - mutation: deleteTransactionLink, - variables: { - id: 12, - }, - }), - ) + describe('with error', () => { + beforeEach(async () => { + spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') + spy.mockImplementation(() => Promise.resolve('some value')) + mockAPIcall.mockRejectedValue({ message: 'Something went wrong :(' }) + await wrapper.find('.test-delete-link .dropdown-item').trigger('click') + }) + + it('toasts an error message', () => { + expect(toastErrorSpy).toBeCalledWith('Something went wrong :(') + }) }) - it('toasts a success message', () => { - expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.deleted') - }) + describe('cancel delete', () => { + beforeEach(async () => { + spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') + spy.mockImplementation(() => Promise.resolve(false)) + mockAPIcall.mockResolvedValue() + await wrapper.find('.test-delete-link .dropdown-item').trigger('click') + }) - it('emits reset transaction link list', () => { - expect(wrapper.emitted('reset-transaction-link-list')).toBeTruthy() - }) - }) - - describe('with error', () => { - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - spy.mockImplementation(() => Promise.resolve('some value')) - mockAPIcall.mockRejectedValue({ message: 'Something went wrong :(' }) - await wrapper.find('.test-delete-link').trigger('click') - }) - - it('toasts an error message', () => { - expect(toastErrorSpy).toBeCalledWith('Something went wrong :(') - }) - }) - - describe('cancel delete', () => { - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - spy.mockImplementation(() => Promise.resolve(false)) - mockAPIcall.mockResolvedValue() - await wrapper.find('.test-delete-link').trigger('click') - }) - - it('does not call the API', () => { - expect(mockAPIcall).not.toBeCalled() + it('does not call the API', () => { + expect(mockAPIcall).not.toBeCalled() + }) }) }) }) diff --git a/frontend/src/components/TransactionLinks/TransactionLink.vue b/frontend/src/components/TransactionLinks/TransactionLink.vue index 66f9f2f92..c7b7682ec 100644 --- a/frontend/src/components/TransactionLinks/TransactionLink.vue +++ b/frontend/src/components/TransactionLinks/TransactionLink.vue @@ -1,55 +1,62 @@ @@ -74,7 +81,7 @@ export default { }, props: { amount: { type: String, required: true }, - code: { type: String, required: true }, + link: { type: String, required: true }, holdAvailableAmount: { type: String, required: true }, id: { type: Number, required: true }, memo: { type: String, required: true }, @@ -88,6 +95,7 @@ export default { this.toastSuccess(this.$t('gdd_per_link.link-copied')) }) .catch(() => { + this.$bvModal.show('modalPopoverCopyError' + this.id) this.toastError(this.$t('gdd_per_link.not-copied')) }) }, @@ -115,8 +123,8 @@ export default { decay() { return `${this.amount - this.holdAvailableAmount}` }, - link() { - return `${window.location.origin}/redeem/${this.code}` + validLink() { + return new Date(this.validUntil) > new Date() }, }, } diff --git a/frontend/src/components/TransactionRows/DateRow.vue b/frontend/src/components/TransactionRows/DateRow.vue index 5f526caaf..5998be134 100644 --- a/frontend/src/components/TransactionRows/DateRow.vue +++ b/frontend/src/components/TransactionRows/DateRow.vue @@ -3,7 +3,7 @@
- {{ diffNow ? $t('gdd_per_link.valid_until') : $t('form.date') }} + {{ text }}
@@ -27,6 +27,20 @@ export default { required: false, default: false, }, + validLink: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + text() { + if (this.diffNow) + return this.validLink + ? this.$t('gdd_per_link.validUntil') + : this.$t('gdd_per_link.expiredOn') + return this.$t('form.date') + }, }, } diff --git a/frontend/src/components/Transactions/TransactionLinkSummary.spec.js b/frontend/src/components/Transactions/TransactionLinkSummary.spec.js index 3e57bceeb..b60cc60f8 100644 --- a/frontend/src/components/Transactions/TransactionLinkSummary.spec.js +++ b/frontend/src/components/Transactions/TransactionLinkSummary.spec.js @@ -44,7 +44,7 @@ describe('TransactionLinkSummary', () => { listTransactionLinks: [ { amount: '75', - code: 'ce28664b5308c17f931c0367', + link: 'http://localhost/redeem/ce28664b5308c17f931c0367', createdAt: '2022-03-16T14:22:40.000Z', holdAvailableAmount: '5.13109484759482747111', id: 86, @@ -55,7 +55,7 @@ describe('TransactionLinkSummary', () => { }, { amount: '85', - code: 'ce28664b5308c17f931c0367', + link: 'http://localhost/redeem/ce28664b5308c17f931c0367', createdAt: '2022-03-16T14:22:40.000Z', holdAvailableAmount: '5.13109484759482747111', id: 107, @@ -65,7 +65,7 @@ describe('TransactionLinkSummary', () => { }, { amount: '95', - code: 'ce28664b5308c17f931c0367', + link: 'http://localhost/redeem/ce28664b5308c17f931c0367', createdAt: '2022-03-16T14:22:40.000Z', holdAvailableAmount: '5.13109484759482747111', id: 92, @@ -76,7 +76,7 @@ describe('TransactionLinkSummary', () => { }, { amount: '150', - code: 'ce28664b5308c17f931c0367', + link: 'http://localhost/redeem/ce28664b5308c17f931c0367', createdAt: '2022-03-16T14:22:40.000Z', holdAvailableAmount: '5.13109484759482747111', id: 16, @@ -132,6 +132,27 @@ describe('TransactionLinkSummary', () => { it('has no component CollapseLinksList', () => { expect(wrapper.findComponent({ name: 'CollapseLinksList' }).isVisible()).toBe(false) }) + + describe('reopen transaction link details', () => { + beforeEach(() => { + jest.clearAllMocks() + wrapper.find('div.transaction-link-details').trigger('click') + }) + + it('calls the API to get the list transaction links', () => { + expect(apolloQueryMock).toBeCalledWith({ + query: listTransactionLinks, + variables: { + currentPage: 1, + }, + fetchPolicy: 'network-only', + }) + }) + + it('has four transactionLinks', () => { + expect(wrapper.vm.transactionLinks).toHaveLength(4) + }) + }) }) describe('load more transaction links', () => { @@ -142,7 +163,7 @@ describe('TransactionLinkSummary', () => { listTransactionLinks: [ { amount: '76', - code: 'ce28664b5308c17f931c0367', + link: 'http://localhost/redeem/ce28664b5308c17f931c0367', createdAt: '2022-03-16T14:22:40.000Z', holdAvailableAmount: '5.13109484759482747111', id: 87, @@ -153,7 +174,7 @@ describe('TransactionLinkSummary', () => { }, { amount: '86', - code: 'ce28664b5308c17f931c0367', + link: 'http://localhost/redeem/ce28664b5308c17f931c0367', createdAt: '2022-03-16T14:22:40.000Z', holdAvailableAmount: '5.13109484759482747111', id: 108, @@ -164,7 +185,7 @@ describe('TransactionLinkSummary', () => { }, { amount: '96', - code: 'ce28664b5308c17f931c0367', + link: 'http://localhost/redeem/ce28664b5308c17f931c0367', createdAt: '2022-03-16T14:22:40.000Z', holdAvailableAmount: '5.13109484759482747111', id: 93, @@ -175,7 +196,7 @@ describe('TransactionLinkSummary', () => { }, { amount: '150', - code: 'ce28664b5308c17f931c0367', + link: 'http://localhost/redeem/ce28664b5308c17f931c0367', createdAt: '2022-03-16T14:22:40.000Z', holdAvailableAmount: '5.13109484759482747111', id: 17, diff --git a/frontend/src/components/Transactions/TransactionLinkSummary.vue b/frontend/src/components/Transactions/TransactionLinkSummary.vue index 84d0f9b84..c24410924 100644 --- a/frontend/src/components/Transactions/TransactionLinkSummary.vue +++ b/frontend/src/components/Transactions/TransactionLinkSummary.vue @@ -83,6 +83,7 @@ export default { if (this.visible) { this.visible = false } else { + this.transactionLinks = [] this.updateListTransactionLinks() this.visible = true } diff --git a/frontend/src/graphql/mutations.js b/frontend/src/graphql/mutations.js index 3dbd8888e..672af5f04 100644 --- a/frontend/src/graphql/mutations.js +++ b/frontend/src/graphql/mutations.js @@ -75,7 +75,7 @@ export const sendCoins = gql` export const createTransactionLink = gql` mutation($amount: Decimal!, $memo: String!) { createTransactionLink(amount: $amount, memo: $memo) { - code + link } } ` diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 3d5a6a95c..2bd905e5e 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -139,7 +139,7 @@ export const listTransactionLinks = gql` amount holdAvailableAmount memo - code + link createdAt validUntil redeemedAt diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 20ce055d4..e9ad958cf 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -102,6 +102,7 @@ "decay-14-day": "VergĂ€nglichkeit fĂŒr 14 Tage", "delete-the-link": "Den Link löschen?", "deleted": "Der Link wurde gelöscht!", + "expiredOn": "Abgelaufen am", "has-account": "Du besitzt bereits ein Gradido Konto?", "header": "Gradidos versenden per Link", "isFree": "Gradido ist weltweit kostenfrei.", @@ -113,7 +114,7 @@ "links_sum": "Offene Links und QR-Codes", "no-account": "Du besitzt noch kein Gradido Konto?", "no-redeem": "Du darfst deinen eigenen Link nicht einlösen!", - "not-copied": "Konnte den Link nicht kopieren: {err}", + "not-copied": "Dein GerĂ€t lĂ€sst das Kopieren leider nicht zu! Bitte kopiere den Link von Hand!", "redeem": "Einlösen", "redeem-text": "Willst du den Betrag jetzt einlösen?", "redeemed": "Erfolgreich eingelöst! Deinem Konto wurden {n} GDD gutgeschrieben.", @@ -121,7 +122,7 @@ "redeemed-title": "eingelöst", "to-login": "Log dich ein", "to-register": "Registriere ein neues Konto.", - "valid_until": "GĂŒltig bis" + "validUntil": "GĂŒltig bis" }, "gdt": { "calculation": "Berechnung der Gradido Transform", @@ -164,6 +165,7 @@ "infoText": "Wenn dir dein Empfehlungsgeber seine Publisher-Id gegeben hat, trage sie hier ein, sonst lass das Feld bitte unverĂ€ndert!", "publisherId": "Publisher-Id:" }, + "qrCode": "QR Code", "send_gdd": "GDD versenden", "send_per_link": "GDD versenden per Link", "settings": { diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 201c44d93..2c53837d0 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -102,6 +102,7 @@ "decay-14-day": "Decay for 14 days", "delete-the-link": "Delete the link?", "deleted": "The link was deleted!", + "expiredOn": "Expired on", "has-account": "You already have a Gradido account?", "header": "Send Gradidos via link", "isFree": "Gradido is free of charge worldwide.", @@ -113,7 +114,7 @@ "links_sum": "Open links and QR codes", "no-account": "You don't have a Gradido account yet?", "no-redeem": "You not allowed to redeem your own link!", - "not-copied": "Could not copy link: {err}", + "not-copied": "Unfortunately, your device does not allow copying! Please copy the link by hand!", "redeem": "Redeem", "redeem-text": "Do you want to redeem the amount now?", "redeemed": "Successfully redeemed! Your account has been credited with {n} GDD.", @@ -121,7 +122,7 @@ "redeemed-title": "redeemed", "to-login": "Log in", "to-register": "Register a new account.", - "valid_until": "Valid until" + "validUntil": "Valid until" }, "gdt": { "calculation": "Calculation of Gradido Transform", @@ -164,6 +165,7 @@ "infoText": "If your referrer has given you his publisher id, enter it here, otherwise leave the field unchanged!", "publisherId": "PublisherID:" }, + "qrCode": "QR Code", "send_gdd": "GDD send", "send_per_link": "GDD send via link", "settings": { diff --git a/frontend/src/pages/Send.spec.js b/frontend/src/pages/Send.spec.js index 447fdde33..88759c839 100644 --- a/frontend/src/pages/Send.spec.js +++ b/frontend/src/pages/Send.spec.js @@ -162,7 +162,11 @@ describe('Send', () => { describe('transaction form link', () => { beforeEach(async () => { apolloMutationMock.mockResolvedValue({ - data: { createTransactionLink: { code: '0123456789' } }, + data: { + createTransactionLink: { + link: 'http://localhost/redeem/0123456789', + }, + }, }) const transactionForm = wrapper.findComponent({ name: 'TransactionForm' }) await transactionForm.findAll('input[type="radio"]').at(1).setChecked() @@ -249,7 +253,7 @@ describe('Send', () => { describe('close button click', () => { beforeEach(async () => { - await wrapper.findAll('button').at(1).trigger('click') + await wrapper.findAll('button').at(2).trigger('click') }) it('Shows the TransactionForm', () => { diff --git a/frontend/src/pages/Send.vue b/frontend/src/pages/Send.vue index 613342dba..cd5f8f572 100644 --- a/frontend/src/pages/Send.vue +++ b/frontend/src/pages/Send.vue @@ -41,7 +41,7 @@ >
@@ -87,7 +87,7 @@ export default { errorResult: '', currentTransactionStep: TRANSACTION_STEPS.transactionForm, loading: false, - code: null, + link: null, } }, props: { @@ -144,7 +144,7 @@ export default { }) .then((result) => { this.$emit('set-tunneled-email', null) - this.code = result.data.createTransactionLink.code + this.link = result.data.createTransactionLink.link this.transactionData = { ...EMPTY_TRANSACTION_DATA } this.currentTransactionStep = TRANSACTION_STEPS.transactionResultLink this.updateTransactions({}) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index cc7868ce3..b7648c9c8 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -5276,6 +5276,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + cross-spawn@^5.0.1, cross-spawn@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -5296,7 +5303,7 @@ cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.2: +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== diff --git a/package.json b/package.json index 2ff37c71a..c8abef594 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gradido", - "version": "1.7.1", + "version": "1.8.0", "description": "Gradido", "main": "index.js", "repository": "git@github.com:gradido/gradido.git",