diff --git a/CHANGELOG.md b/CHANGELOG.md index 49fdfd07f..63b0c2c90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,55 @@ 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.11.0](https://github.com/gradido/gradido/compare/1.10.1...1.11.0) + +- Fix navbar community item [`#2102`](https://github.com/gradido/gradido/pull/2102) +- Add validation date info to copied text after transaction link creation [`#2101`](https://github.com/gradido/gradido/pull/2101) +- Remove member area from menu (desktop and mobile), when user has no elopage account [`#2099`](https://github.com/gradido/gradido/pull/2099) +- fix: Use Inner Join for Contribution and User [`#2100`](https://github.com/gradido/gradido/pull/2100) +- Enable copying the link, username, amount, and memo text after transaction link creation [`#2098`](https://github.com/gradido/gradido/pull/2098) +- feat: Insert Missing Contributions Migration [`#2053`](https://github.com/gradido/gradido/pull/2053) +- [Bug] Wallet improvments for Contributions [`#2090`](https://github.com/gradido/gradido/pull/2090) +- [Fix] Add createdAt & contributionDate to ContributionListItems [`#2093`](https://github.com/gradido/gradido/pull/2093) +- fix: 🍰 Reset Amount In Contribution Form And Write A Test [`#2086`](https://github.com/gradido/gradido/pull/2086) +- [Feat] Replace logic to validation-provider. [`#2088`](https://github.com/gradido/gradido/pull/2088) +- fix: Add Confirm Dialog on Delete Contribution [`#2087`](https://github.com/gradido/gradido/pull/2087) +- fix: Admin Cannot Edit User Contribution [`#2085`](https://github.com/gradido/gradido/pull/2085) +- fix: Update contribution_date when Moved by Seed [`#2083`](https://github.com/gradido/gradido/pull/2083) +- chore: 🍰 Provide Volume For Backend Log-Files In Docker [`#2067`](https://github.com/gradido/gradido/pull/2067) +- feat: 🍰 Community Contribution Site And Form [`#2042`](https://github.com/gradido/gradido/pull/2042) +- [Refactor] Move MEMO_MIN_CHARS and MEMO_MAX_CHARS to const file [`#2082`](https://github.com/gradido/gradido/pull/2082) +- Fix: Test memo length on createContribution & updateContribution [`#2080`](https://github.com/gradido/gradido/pull/2080) +- chore: 🍰 Change `image` Entries In Docker Compose Files And Get Apple M1 Running [`#2050`](https://github.com/gradido/gradido/pull/2050) +- fix: Windows 0D 0A Linebreaks to Unix 0A [`#2064`](https://github.com/gradido/gradido/pull/2064) +- Add contributionDate to the Contribution object. [`#2066`](https://github.com/gradido/gradido/pull/2066) +- fix: Add Contributions to User [`#2062`](https://github.com/gradido/gradido/pull/2062) +- Feat: ContributionResolver - delete mutation [`#2035`](https://github.com/gradido/gradido/pull/2035) +- Fix: Add count to list contributions [`#2061`](https://github.com/gradido/gradido/pull/2061) +- docu: Explain how `.env` Files are Working [`#2022`](https://github.com/gradido/gradido/pull/2022) +- [WIP] 1794 feature event protocol 1 implement the basics of the business event protocol [`#1997`](https://github.com/gradido/gradido/pull/1997) +- docs: 🍰 Document The Setup Of The GraphQL Playground [`#2060`](https://github.com/gradido/gradido/pull/2060) +- Feat: List all contribution [`#2057`](https://github.com/gradido/gradido/pull/2057) +- feat: Do not log IntrospectionQuery from Query Browser [`#2059`](https://github.com/gradido/gradido/pull/2059) +- Feat: Add confirmedBy and confirmedAt for the contribution query. [`#2052`](https://github.com/gradido/gradido/pull/2052) +- Add open creations to webapp [`#2048`](https://github.com/gradido/gradido/pull/2048) +- Prevent session expiration modal from displaying negative seconds, when session is expired for more than 0 seconds [`#2054`](https://github.com/gradido/gradido/pull/2054) +- feat: mutation contribution update [`#2032`](https://github.com/gradido/gradido/pull/2032) +- feat: Login Returns Open Creations for User [`#2046`](https://github.com/gradido/gradido/pull/2046) +- Migrate transaction to valid dataset for gradido node [`#2029`](https://github.com/gradido/gradido/pull/2029) +- feat: implement contribution list query [`#2031`](https://github.com/gradido/gradido/pull/2031) +- add code for moving user creation date if transaction before exist [`#2034`](https://github.com/gradido/gradido/pull/2034) +- change text from page [`#2037`](https://github.com/gradido/gradido/pull/2037) +- Transaction link: copy link, text and more [`#2030`](https://github.com/gradido/gradido/pull/2030) +- change welcome in community text [`#2025`](https://github.com/gradido/gradido/pull/2025) +- changed link color in navbar and language switch [`#2024`](https://github.com/gradido/gradido/pull/2024) +- feat: ContributionResolver - createContribution [`#2009`](https://github.com/gradido/gradido/pull/2009) + #### [1.10.1](https://github.com/gradido/gradido/compare/1.10.0...1.10.1) +> 30 June 2022 + +- release: 1.10.1 [`#2021`](https://github.com/gradido/gradido/pull/2021) - automatic session logout with info modal [`#2001`](https://github.com/gradido/gradido/pull/2001) - 1910 separate text for the slideshow images. [`#1998`](https://github.com/gradido/gradido/pull/1998) - Origin/1921 additional parameter checks for createContributionLinks [`#1996`](https://github.com/gradido/gradido/pull/1996) diff --git a/admin/jest.config.js b/admin/jest.config.js index 9b9842bad..3e416e7b6 100644 --- a/admin/jest.config.js +++ b/admin/jest.config.js @@ -26,5 +26,5 @@ module.exports = { testMatch: ['**/?(*.)+(spec|test).js?(x)'], // snapshotSerializers: ['jest-serializer-vue'], transformIgnorePatterns: ['/node_modules/(?!vee-validate/dist/rules)'], - testEnvironment: 'jest-environment-jsdom-sixteen', + // testEnvironment: 'jest-environment-jsdom-sixteen', // not needed anymore since jest@26, see: https://www.npmjs.com/package/jest-environment-jsdom-sixteen } diff --git a/admin/package.json b/admin/package.json index 50145d44a..98746be6d 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.10.1", + "version": "1.11.0", "license": "Apache-2.0", "private": false, "scripts": { @@ -70,7 +70,6 @@ "eslint-plugin-prettier": "3.3.1", "eslint-plugin-promise": "^5.1.1", "eslint-plugin-vue": "^7.20.0", - "jest-environment-jsdom-sixteen": "^2.0.0", "postcss": "^8.4.8", "postcss-html": "^1.3.0", "postcss-scss": "^4.0.3", diff --git a/admin/src/components/TransactionLinkList.vue b/admin/src/components/TransactionLinkList.vue index 0289792a2..d850083cc 100644 --- a/admin/src/components/TransactionLinkList.vue +++ b/admin/src/components/TransactionLinkList.vue @@ -11,6 +11,7 @@ :per-page="perPage" :total-rows="rows" align="center" + :hide-ellipsis="true" > diff --git a/admin/src/graphql/communityStatistics.js b/admin/src/graphql/communityStatistics.js new file mode 100644 index 000000000..868bfd02a --- /dev/null +++ b/admin/src/graphql/communityStatistics.js @@ -0,0 +1,15 @@ +import gql from 'graphql-tag' + +export const communityStatistics = gql` + query { + communityStatistics { + totalUsers + activeUsers + deletedUsers + totalGradidoCreated + totalGradidoDecayed + totalGradidoAvailable + totalGradidoUnbookedDecayed + } + } +` diff --git a/admin/src/pages/Creation.vue b/admin/src/pages/Creation.vue index 9e554ff92..26d44fd3e 100644 --- a/admin/src/pages/Creation.vue +++ b/admin/src/pages/Creation.vue @@ -29,6 +29,7 @@ per-page="perPage" :total-rows="rows" align="center" + :hide-ellipsis="true" > diff --git a/admin/src/pages/UserSearch.vue b/admin/src/pages/UserSearch.vue index 8eb1f9c63..df49526d7 100644 --- a/admin/src/pages/UserSearch.vue +++ b/admin/src/pages/UserSearch.vue @@ -52,6 +52,7 @@ per-page="perPage" :total-rows="rows" align="center" + :hide-ellipsis="true" >
diff --git a/admin/yarn.lock b/admin/yarn.lock index 09b543354..b5b76cee8 100644 --- a/admin/yarn.lock +++ b/admin/yarn.lock @@ -1282,17 +1282,6 @@ jest-message-util "^24.9.0" jest-mock "^24.9.0" -"@jest/fake-timers@^25.1.0": - version "25.5.0" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-25.5.0.tgz#46352e00533c024c90c2bc2ad9f2959f7f114185" - integrity sha512-9y2+uGnESw/oyOI3eww9yaxdZyHq7XvprfP/eeoCsjqKYts2yRlsHS/SgjPDV8FyMfn2nbMy8YzUk6nyvdLOpQ== - dependencies: - "@jest/types" "^25.5.0" - jest-message-util "^25.5.0" - jest-mock "^25.5.0" - jest-util "^25.5.0" - lolex "^5.0.0" - "@jest/fake-timers@^26.6.2": version "26.6.2" resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.6.2.tgz#459c329bcf70cee4af4d7e3f3e67848123535aad" @@ -1504,16 +1493,6 @@ "@types/istanbul-reports" "^1.1.1" "@types/yargs" "^13.0.0" -"@jest/types@^25.5.0": - version "25.5.0" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.5.0.tgz#4d6a4793f7b9599fc3680877b856a97dbccf2a9d" - integrity sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^1.1.1" - "@types/yargs" "^15.0.0" - chalk "^3.0.0" - "@jest/types@^26.6.2": version "26.6.2" resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" @@ -4130,14 +4109,6 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4 escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" - integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - chalk@^4.0.0, chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -8012,16 +7983,6 @@ jest-environment-jsdom-fifteen@^1.0.2: jest-util "^24.0.0" jsdom "^15.2.1" -jest-environment-jsdom-sixteen@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom-sixteen/-/jest-environment-jsdom-sixteen-2.0.0.tgz#0f8c12663ccd9836d248574decffc575bfb091e1" - integrity sha512-BF+8P67aEJcd78TQzwSb9P4a73cArOWb5KgqI8eU6cHRWDIJdDRE8XTeZAmOuDSDhKpuEXjKkXwWB3GOJvqHJQ== - dependencies: - "@jest/fake-timers" "^25.1.0" - jest-mock "^25.1.0" - jest-util "^25.1.0" - jsdom "^16.2.1" - jest-environment-jsdom@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz#4b0806c7fc94f95edb369a69cc2778eec2b7375b" @@ -8236,20 +8197,6 @@ jest-message-util@^24.9.0: slash "^2.0.0" stack-utils "^1.0.1" -jest-message-util@^25.5.0: - version "25.5.0" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-25.5.0.tgz#ea11d93204cc7ae97456e1d8716251185b8880ea" - integrity sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA== - dependencies: - "@babel/code-frame" "^7.0.0" - "@jest/types" "^25.5.0" - "@types/stack-utils" "^1.0.1" - chalk "^3.0.0" - graceful-fs "^4.2.4" - micromatch "^4.0.2" - slash "^3.0.0" - stack-utils "^1.0.1" - jest-message-util@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" @@ -8272,13 +8219,6 @@ jest-mock@^24.0.0, jest-mock@^24.9.0: dependencies: "@jest/types" "^24.9.0" -jest-mock@^25.1.0, jest-mock@^25.5.0: - version "25.5.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-25.5.0.tgz#a91a54dabd14e37ecd61665d6b6e06360a55387a" - integrity sha512-eXWuTV8mKzp/ovHc5+3USJMYsTBhyQ+5A1Mak35dey/RG8GlM4YWVylZuGgVXinaW6tpvk/RSecmF37FKUlpXA== - dependencies: - "@jest/types" "^25.5.0" - jest-mock@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.2.tgz#d6cb712b041ed47fe0d9b6fc3474bc6543feb302" @@ -8555,17 +8495,6 @@ jest-util@^24.0.0, jest-util@^24.9.0: slash "^2.0.0" source-map "^0.6.0" -jest-util@^25.1.0, jest-util@^25.5.0: - version "25.5.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-25.5.0.tgz#31c63b5d6e901274d264a4fec849230aa3fa35b0" - integrity sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA== - dependencies: - "@jest/types" "^25.5.0" - chalk "^3.0.0" - graceful-fs "^4.2.4" - is-ci "^2.0.0" - make-dir "^3.0.0" - jest-util@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" @@ -8812,7 +8741,7 @@ jsdom@^15.2.1: ws "^7.0.0" xml-name-validator "^3.0.0" -jsdom@^16.2.1, jsdom@^16.4.0: +jsdom@^16.4.0: version "16.7.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw== @@ -9167,13 +9096,6 @@ loglevel@^1.6.8: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197" integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw== -lolex@^5.0.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/lolex/-/lolex-5.1.2.tgz#953694d098ce7c07bc5ed6d0e42bc6c0c6d5a367" - integrity sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A== - dependencies: - "@sinonjs/commons" "^1.7.0" - loose-envify@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" diff --git a/backend/log4js-config.json b/backend/log4js-config.json index 451da56ab..848a4fa79 100644 --- a/backend/log4js-config.json +++ b/backend/log4js-config.json @@ -25,6 +25,14 @@ "keepFileExt" : true, "fileNameSep" : "_" }, + "klicktipp": + { + "type": "dateFile", + "filename": "../logs/backend/klicktipp.log", + "pattern": "%d{ISO8601} %p %c %X{user} %f:%l %m", + "keepFileExt" : true, + "fileNameSep" : "_" + }, "errorFile": { "type": "dateFile", @@ -90,6 +98,17 @@ "level": "debug", "enableCallStack": true }, + "klicktipp": + { + "appenders": + [ + "klicktipp", + "out", + "errors" + ], + "level": "debug", + "enableCallStack": true + }, "http": { "appenders": diff --git a/backend/package.json b/backend/package.json index bb4ab3e51..d31d12eda 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "gradido-backend", - "version": "1.10.1", + "version": "1.11.0", "description": "Gradido unified backend providing an API-Service for Gradido Transactions", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/backend", @@ -40,7 +40,8 @@ "reflect-metadata": "^0.1.13", "sodium-native": "^3.3.0", "ts-jest": "^27.0.5", - "type-graphql": "^1.1.1" + "type-graphql": "^1.1.1", + "uuid": "^8.3.2" }, "devDependencies": { "@types/express": "^4.17.12", diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index d5e2cc7ce..0d8252402 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -30,6 +30,9 @@ export enum RIGHTS { LIST_CONTRIBUTIONS = 'LIST_CONTRIBUTIONS', LIST_ALL_CONTRIBUTIONS = 'LIST_ALL_CONTRIBUTIONS', UPDATE_CONTRIBUTION = 'UPDATE_CONTRIBUTION', + LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS', + COMMUNITY_STATISTICS = 'COMMUNITY_STATISTICS', + SEARCH_ADMIN_USERS = 'SEARCH_ADMIN_USERS', // Admin SEARCH_USERS = 'SEARCH_USERS', SET_USER_ROLE = 'SET_USER_ROLE', @@ -45,7 +48,6 @@ export enum RIGHTS { CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST', LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN', CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK', - LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS', DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK', UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK', } diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts index 9dcba0a4b..f14e77b17 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -28,6 +28,9 @@ export const ROLE_USER = new Role('user', [ RIGHTS.LIST_CONTRIBUTIONS, RIGHTS.LIST_ALL_CONTRIBUTIONS, RIGHTS.UPDATE_CONTRIBUTION, + RIGHTS.SEARCH_ADMIN_USERS, + RIGHTS.LIST_CONTRIBUTION_LINKS, + RIGHTS.COMMUNITY_STATISTICS, ]) export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 677090ee2..282d458ce 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0045-adapt_users_table_for_gradidoid', + DB_VERSION: '0046-adapt_users_table_for_gradidoid', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info diff --git a/backend/src/graphql/enum/ContributionStatus.ts b/backend/src/graphql/enum/ContributionStatus.ts new file mode 100644 index 000000000..67cdf5398 --- /dev/null +++ b/backend/src/graphql/enum/ContributionStatus.ts @@ -0,0 +1,14 @@ +import { registerEnumType } from 'type-graphql' + +export enum ContributionStatus { + PENDING = 'PENDING', + DELETED = 'DELETED', + IN_PROGRESS = 'IN_PROGRESS', + DENIED = 'DENIED', + CONFIRMED = 'CONFIRMED', +} + +registerEnumType(ContributionStatus, { + name: 'ContributionStatus', + description: 'Name of the Type of the Contribution Status', +}) diff --git a/backend/src/graphql/enum/ContributionType.ts b/backend/src/graphql/enum/ContributionType.ts new file mode 100644 index 000000000..e8529edc4 --- /dev/null +++ b/backend/src/graphql/enum/ContributionType.ts @@ -0,0 +1,12 @@ +import { registerEnumType } from 'type-graphql' + +export enum ContributionType { + ADMIN = 'ADMIN', + USER = 'USER', + LINK = 'LINK', +} + +registerEnumType(ContributionType, { + name: 'ContributionType', + description: 'Name of the Type of the Contribution', +}) diff --git a/backend/src/graphql/model/AdminUser.ts b/backend/src/graphql/model/AdminUser.ts new file mode 100644 index 000000000..92a22b7f1 --- /dev/null +++ b/backend/src/graphql/model/AdminUser.ts @@ -0,0 +1,25 @@ +import { User } from '@entity/User' +import { Field, Int, ObjectType } from 'type-graphql' + +@ObjectType() +export class AdminUser { + constructor(user: User) { + this.firstName = user.firstName + this.lastName = user.lastName + } + + @Field(() => String) + firstName: string + + @Field(() => String) + lastName: string +} + +@ObjectType() +export class SearchAdminUsersResult { + @Field(() => Int) + userCount: number + + @Field(() => [AdminUser]) + userList: AdminUser[] +} diff --git a/backend/src/graphql/model/CommunityStatistics.ts b/backend/src/graphql/model/CommunityStatistics.ts new file mode 100644 index 000000000..61354115c --- /dev/null +++ b/backend/src/graphql/model/CommunityStatistics.ts @@ -0,0 +1,26 @@ +import { ObjectType, Field } from 'type-graphql' +import Decimal from 'decimal.js-light' + +@ObjectType() +export class CommunityStatistics { + @Field(() => Number) + totalUsers: number + + @Field(() => Number) + activeUsers: number + + @Field(() => Number) + deletedUsers: number + + @Field(() => Decimal) + totalGradidoCreated: Decimal + + @Field(() => Decimal) + totalGradidoDecayed: Decimal + + @Field(() => Decimal) + totalGradidoAvailable: Decimal + + @Field(() => Decimal) + totalGradidoUnbookedDecayed: Decimal +} diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index 3ea4e2c05..728851ec2 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -8,6 +8,9 @@ import { FULL_CREATION_AVAILABLE } from '../resolver/const/const' export class User { constructor(user: dbUser, creation: Decimal[] = FULL_CREATION_AVAILABLE) { this.id = user.id + this.gradidoID = user.gradidoID + this.alias = user.alias + // this.email = user.email this.email = user.emailContact.email this.firstName = user.firstName this.lastName = user.lastName @@ -28,6 +31,12 @@ export class User { // `public_key` binary(32) DEFAULT NULL, // `privkey` binary(80) DEFAULT NULL, + @Field(() => String) + gradidoID: string + + @Field(() => String, { nullable: true }) + alias: string + // TODO privacy issue here @Field(() => String) email: string diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 7b1c6ffcd..f0ce064b4 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -1857,11 +1857,17 @@ describe('AdminResolver', () => { }) }) + // TODO: Set this test in new location to have datas describe('listContributionLinks', () => { - it('returns an error', async () => { + it('returns an empty object', async () => { await expect(query({ query: listContributionLinks })).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('401 Unauthorized')], + data: { + listContributionLinks: { + count: 0, + links: [], + }, + }, }), ) }) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 84ae09cf8..e70fe71ee 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -36,6 +36,8 @@ import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { User as dbUser } from '@entity/User' import { User } from '@model/User' import { TransactionTypeId } from '@enum/TransactionTypeId' +import { ContributionType } from '@enum/ContributionType' +import { ContributionStatus } from '@enum/ContributionStatus' import Decimal from 'decimal.js-light' import { Decay } from '@model/Decay' import Paginated from '@arg/Paginated' @@ -260,6 +262,8 @@ export class AdminResolver { contribution.contributionDate = creationDateObj contribution.memo = memo contribution.moderatorId = moderator.id + contribution.contributionType = ContributionType.ADMIN + contribution.contributionStatus = ContributionStatus.PENDING logger.trace('contribution to save', contribution) await Contribution.save(contribution) @@ -337,6 +341,7 @@ export class AdminResolver { contributionToUpdate.memo = memo contributionToUpdate.contributionDate = new Date(creationDate) contributionToUpdate.moderatorId = moderator.id + contributionToUpdate.contributionStatus = ContributionStatus.PENDING await Contribution.save(contributionToUpdate) const result = new AdminUpdateContribution() @@ -387,6 +392,8 @@ export class AdminResolver { if (!contribution) { throw new Error('Contribution not found for given id.') } + contribution.contributionStatus = ContributionStatus.DELETED + await contribution.save() const res = await contribution.softRemove() return !!res } @@ -454,6 +461,7 @@ export class AdminResolver { contribution.confirmedAt = receivedCallDate contribution.confirmedBy = moderatorUser.id contribution.transactionId = transaction.id + contribution.contributionStatus = ContributionStatus.CONFIRMED await queryRunner.manager.update(Contribution, { id: contribution.id }, contribution) await queryRunner.commitTransaction() @@ -501,7 +509,7 @@ export class AdminResolver { order: { updatedAt: 'DESC' }, }) - optInCode = await checkOptInCode(optInCode, user.id) + optInCode = await checkOptInCode(optInCode, user) // eslint-disable-next-line @typescript-eslint/no-unused-vars const emailSent = await sendAccountActivationEmail({ diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index a22715fb4..8056ffde3 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -3,10 +3,12 @@ import { Context, getUser } from '@/server/context' import { backendLogger as logger } from '@/server/logger' import { Contribution as dbContribution } from '@entity/Contribution' import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql' -import { FindOperator, IsNull } from '@dbTools/typeorm' +import { FindOperator, IsNull, getConnection } from '@dbTools/typeorm' import ContributionArgs from '@arg/ContributionArgs' import Paginated from '@arg/Paginated' import { Order } from '@enum/Order' +import { ContributionType } from '@enum/ContributionType' +import { ContributionStatus } from '@enum/ContributionStatus' import { Contribution, ContributionListResult } from '@model/Contribution' import { UnconfirmedContribution } from '@model/UnconfirmedContribution' import { User } from '@model/User' @@ -43,6 +45,8 @@ export class ContributionResolver { contribution.createdAt = new Date() contribution.contributionDate = creationDateObj contribution.memo = memo + contribution.contributionType = ContributionType.USER + contribution.contributionStatus = ContributionStatus.PENDING logger.trace('contribution to save', contribution) await dbContribution.save(contribution) @@ -66,6 +70,8 @@ export class ContributionResolver { if (contribution.confirmedAt) { throw new Error('A confirmed contribution can not be deleted') } + contribution.contributionStatus = ContributionStatus.DELETED + await contribution.save() const res = await contribution.softRemove() return !!res } @@ -106,14 +112,15 @@ export class ContributionResolver { @Args() { currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated, ): Promise { - const [dbContributions, count] = await dbContribution.findAndCount({ - relations: ['user'], - order: { - createdAt: order, - }, - skip: (currentPage - 1) * pageSize, - take: pageSize, - }) + const [dbContributions, count] = await getConnection() + .createQueryBuilder() + .select('c') + .from(dbContribution, 'c') + .innerJoinAndSelect('c.user', 'u') + .orderBy('c.createdAt', order) + .limit(pageSize) + .offset((currentPage - 1) * pageSize) + .getManyAndCount() return new ContributionListResult( count, dbContributions.map( @@ -163,6 +170,7 @@ export class ContributionResolver { contributionToUpdate.amount = amount contributionToUpdate.memo = memo contributionToUpdate.contributionDate = new Date(creationDate) + contributionToUpdate.contributionStatus = ContributionStatus.PENDING dbContribution.save(contributionToUpdate) return new UnconfirmedContribution(contributionToUpdate, user, creations) diff --git a/backend/src/graphql/resolver/StatisticsResolver.ts b/backend/src/graphql/resolver/StatisticsResolver.ts new file mode 100644 index 000000000..4c1500839 --- /dev/null +++ b/backend/src/graphql/resolver/StatisticsResolver.ts @@ -0,0 +1,77 @@ +import { Resolver, Query, Authorized } from 'type-graphql' +import { RIGHTS } from '@/auth/RIGHTS' +import { CommunityStatistics } from '@model/CommunityStatistics' +import { User as DbUser } from '@entity/User' +import { Transaction as DbTransaction } from '@entity/Transaction' +import { getConnection } from '@dbTools/typeorm' +import Decimal from 'decimal.js-light' +import { calculateDecay } from '@/util/decay' + +@Resolver() +export class StatisticsResolver { + @Authorized([RIGHTS.COMMUNITY_STATISTICS]) + @Query(() => CommunityStatistics) + async communityStatistics(): Promise { + const allUsers = await DbUser.find({ withDeleted: true }) + + let totalUsers = 0 + let activeUsers = 0 + let deletedUsers = 0 + + let totalGradidoAvailable: Decimal = new Decimal(0) + let totalGradidoUnbookedDecayed: Decimal = new Decimal(0) + + const receivedCallDate = new Date() + + for (let i = 0; i < allUsers.length; i++) { + if (allUsers[i].deletedAt) { + deletedUsers++ + } else { + totalUsers++ + const lastTransaction = await DbTransaction.findOne({ + where: { userId: allUsers[i].id }, + order: { balanceDate: 'DESC' }, + }) + if (lastTransaction) { + activeUsers++ + const decay = calculateDecay( + lastTransaction.balance, + lastTransaction.balanceDate, + receivedCallDate, + ) + if (decay) { + totalGradidoAvailable = totalGradidoAvailable.plus(decay.balance.toString()) + totalGradidoUnbookedDecayed = totalGradidoUnbookedDecayed.plus(decay.decay.toString()) + } + } + } + } + + const queryRunner = getConnection().createQueryRunner() + await queryRunner.connect() + + const { totalGradidoCreated } = await queryRunner.manager + .createQueryBuilder() + .select('SUM(transaction.amount) AS totalGradidoCreated') + .from(DbTransaction, 'transaction') + .where('transaction.typeId = 1') + .getRawOne() + + const { totalGradidoDecayed } = await queryRunner.manager + .createQueryBuilder() + .select('SUM(transaction.decay) AS totalGradidoDecayed') + .from(DbTransaction, 'transaction') + .where('transaction.decay IS NOT NULL') + .getRawOne() + + return { + totalUsers, + activeUsers, + deletedUsers, + totalGradidoCreated, + totalGradidoDecayed, + totalGradidoAvailable, + totalGradidoUnbookedDecayed, + } + } +} diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 8696065ed..ccc0f628d 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -26,6 +26,8 @@ import { User } from '@model/User' import { calculateDecay } from '@/util/decay' import { executeTransaction } from './TransactionResolver' import { Order } from '@enum/Order' +import { ContributionType } from '@enum/ContributionType' +import { ContributionStatus } from '@enum/ContributionStatus' import { Contribution as DbContribution } from '@entity/Contribution' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' import { getUserCreation, validateContribution } from './util/creations' @@ -231,6 +233,9 @@ export class TransactionLinkResolver { contribution.memo = contributionLink.memo contribution.amount = contributionLink.amount contribution.contributionLinkId = contributionLink.id + contribution.contributionType = ContributionType.LINK + contribution.contributionStatus = ContributionStatus.CONFIRMED + await queryRunner.manager.insert(DbContribution, contribution) const lastTransaction = await queryRunner.manager diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 9034df8f6..0c6189e84 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -5,7 +5,7 @@ import { testEnvironment, headerPushMock, resetToken, cleanDB, resetEntity } fro import { userFactory } from '@/seeds/factory/user' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { createUser, setPassword, forgotPassword, updateUserInfos } from '@/seeds/graphql/mutations' -import { login, logout, verifyLogin, queryOptIn } from '@/seeds/graphql/queries' +import { login, logout, verifyLogin, queryOptIn, searchAdminUsers } from '@/seeds/graphql/queries' import { GraphQLError } from 'graphql' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { User } from '@entity/User' @@ -20,6 +20,8 @@ import { ContributionLink } from '@model/ContributionLink' // import { TransactionLink } from '@entity/TransactionLink' import { logger } from '@test/testSetup' +import { validate as validateUUID, version as versionUUID } from 'uuid' +import { peterLustig } from '@/seeds/users/peter-lustig' // import { klicktippSignIn } from '@/apis/KlicktippController' @@ -132,6 +134,10 @@ describe('UserResolver', () => { contributionLinkId: null, }, ]) + const valUUID = validateUUID(user[0].gradidoID) + const verUUID = versionUUID(user[0].gradidoID) + expect(valUUID).toEqual(true) + expect(verUUID).toEqual(4) }) it('creates an email optin', () => { @@ -201,7 +207,7 @@ describe('UserResolver', () => { it('sets "de" as default language', async () => { await mutate({ mutation: createUser, - variables: { ...variables, email: 'bibi@bloxberg.de', language: 'es' }, + variables: { ...variables, email: 'bibi@bloxberg.de', language: 'fr' }, }) await expect(User.find()).resolves.toEqual( expect.arrayContaining([ @@ -874,6 +880,51 @@ bei Gradidio sei dabei!`, }) }) }) + + describe('searchAdminUsers', () => { + describe('unauthenticated', () => { + it('throws an error', async () => { + resetToken() + await expect(mutate({ mutation: searchAdminUsers })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + await userFactory(testEnv, bibiBloxberg) + await userFactory(testEnv, peterLustig) + await query({ + query: login, + variables: { + email: 'bibi@bloxberg.de', + password: 'Aa12345_', + }, + }) + }) + + it('finds peter@lustig.de', async () => { + await expect(mutate({ mutation: searchAdminUsers })).resolves.toEqual( + expect.objectContaining({ + data: { + searchAdminUsers: { + userCount: 1, + userList: expect.arrayContaining([ + expect.objectContaining({ + firstName: 'Peter', + lastName: 'Lustig', + }), + ]), + }, + }, + }), + ) + }) + }) + }) }) describe('printTimeDuration', () => { diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 687cad68b..a2b2b5675 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -2,7 +2,7 @@ import fs from 'fs' import { backendLogger as logger } from '@/server/logger' import { Context, getUser } from '@/server/context' import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql' -import { getConnection } from '@dbTools/typeorm' +import { getConnection, getCustomRepository, IsNull, Not } from '@dbTools/typeorm' import CONFIG from '@/config' import { User } from '@model/User' import { User as DbUser } from '@entity/User' @@ -33,6 +33,11 @@ import { } from '@/event/Event' import { getUserCreation } from './util/creations' import { UserContactType } from '../enum/UserContactType' +import { UserRepository } from '@/typeorm/repository/User' +import { SearchAdminUsersResult } from '@model/AdminUser' +import Paginated from '@arg/Paginated' +import { Order } from '@enum/Order' +import { v4 as uuidv4 } from 'uuid' // eslint-disable-next-line @typescript-eslint/no-var-requires const sodium = require('sodium-native') @@ -44,7 +49,7 @@ const isPassword = (password: string): boolean => { return !!password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^a-zA-Z0-9 \\t\\n\\r]).{8,}$/) } -const LANGUAGES = ['de', 'en'] +const LANGUAGES = ['de', 'en', 'es'] const DEFAULT_LANGUAGE = 'de' const isLanguage = (language: string): boolean => { return LANGUAGES.includes(language) @@ -202,7 +207,7 @@ const newEmailOptIn = (userId: number): LoginEmailOptIn => { // if optIn does not exits, it is created export const checkOptInCode = async ( optInCode: LoginEmailOptIn | undefined, - userId: number, + user: DbUser, optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER, ): Promise => { logger.info(`checkOptInCode... ${optInCode}`) @@ -222,26 +227,33 @@ export const checkOptInCode = async ( optInCode.updatedAt = new Date() optInCode.resendCount++ } else { - logger.trace('create new OptIn for userId=' + userId) - optInCode = newEmailOptIn(userId) + logger.trace('create new OptIn for userId=' + user.id) + optInCode = newEmailOptIn(user.id) + } + + if (user.emailChecked) { + optInCode.emailOptInTypeId = optInType } - optInCode.emailOptInTypeId = optInType await LoginEmailOptIn.save(optInCode).catch(() => { logger.error('Unable to save optin code= ' + optInCode) throw new Error('Unable to save optin code.') }) - logger.debug(`checkOptInCode...successful: ${optInCode} for userid=${userId}`) + logger.debug(`checkOptInCode...successful: ${optInCode} for userid=${user.id}`) return optInCode } */ export const checkEmailVerificationCode = async ( emailContact: DbUserContact, - optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER + optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER, ): Promise => { logger.info(`checkEmailVerificationCode... ${emailContact}`) if (emailContact.updatedAt) { if (!canEmailResend(emailContact.updatedAt)) { - logger.error(`email already sent less than ${printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)} minutes ago`) + logger.error( + `email already sent less than ${printTimeDuration( + CONFIG.EMAIL_CODE_REQUEST_TIME, + )} minutes ago`, + ) throw new Error( `email already sent less than ${printTimeDuration( CONFIG.EMAIL_CODE_REQUEST_TIME, @@ -264,9 +276,22 @@ export const checkEmailVerificationCode = async ( return emailContact } -export const activationLink = (optInCode: LoginEmailOptIn): string => { - logger.debug(`activationLink(${LoginEmailOptIn})...`) - return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, optInCode.verificationCode.toString()) +export const activationLink = (verificationCode: BigInt): string => { + logger.debug(`activationLink(${verificationCode})...`) + return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, verificationCode.toString()) +} + +const newGradidoID = async (): Promise => { + let gradidoId: string + let countIds: number + do { + gradidoId = uuidv4() + countIds = await DbUser.count({ where: { gradidoID: gradidoId } }) + if (countIds > 0) { + logger.info('Gradido-ID creation conflict...') + } + } while (countIds > 0) + return gradidoId } @Resolver() @@ -407,11 +432,13 @@ export class UserResolver { logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`) if (foundUser) { - logger.info('User already exists with this email=' + email) + // ATTENTION: this logger-message will be exactly expected during tests + logger.info(`User already exists with this email=${email}`) // TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent. const user = new User(communityDbUser) user.id = sodium.randombytes_random() % (2048 * 16) // TODO: for a better faking derive id from email so that it will be always the same id when the same email comes in? + user.gradidoID = uuidv4() user.email = email user.firstName = firstName user.lastName = lastName @@ -441,6 +468,7 @@ export class UserResolver { // const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash // const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1]) // const emailHash = getEmailHash(email) + const gradidoID = await newGradidoID() const eventRegister = new EventRegister() const eventRedeemRegister = new EventRedeemRegister() @@ -450,6 +478,8 @@ export class UserResolver { const dbUser = new DbUser() // dbUser.emailContact = dbEmailContact + dbUser.gradidoID = gradidoID + // dbUser.email = email dbUser.firstName = firstName dbUser.lastName = lastName // dbUser.emailHash = emailHash @@ -572,14 +602,17 @@ export class UserResolver { // let optInCode = await LoginEmailOptIn.findOne({ // userId: user.id, // }) - let optInCode = user.emailContact.emailVerificationCode - optInCode = await checkEmailVerificationCode(user.emailContact, OptInType.EMAIL_OPT_IN_RESET_PASSWORD) + // let optInCode = user.emailContact.emailVerificationCode + const dbUserContact = await checkEmailVerificationCode( + user.emailContact, + OptInType.EMAIL_OPT_IN_RESET_PASSWORD, + ) - optInCode = await checkOptInCode(optInCode, user.id, OptInType.EMAIL_OPT_IN_RESET_PASSWORD) - logger.info(`optInCode for ${email}=${optInCode}`) + // optInCode = await checkOptInCode(optInCode, user, OptInType.EMAIL_OPT_IN_RESET_PASSWORD) + logger.info(`optInCode for ${email}=${dbUserContact}`) // eslint-disable-next-line @typescript-eslint/no-unused-vars const emailSent = await sendResetPasswordEmailMailer({ - link: activationLink(optInCode), + link: activationLink(dbUserContact.emailVerificationCode), firstName: user.firstName, lastName: user.lastName, email, @@ -589,7 +622,7 @@ export class UserResolver { /* uncomment this, when you need the activation link on the console */ // In case EMails are disabled log the activation link for the user if (!emailSent) { - logger.debug(`Reset password link: ${activationLink(optInCode)}`) + logger.debug(`Reset password link: ${activationLink(dbUserContact.emailVerificationCode)}`) } logger.info(`forgotPassword(${email}) successful...`) @@ -611,13 +644,21 @@ export class UserResolver { } // Load code + /* const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => { logger.error('Could not login with emailVerificationCode') throw new Error('Could not login with emailVerificationCode') }) - logger.debug('optInCode loaded...') + */ + const userContact = await DbUserContact.findOneOrFail({ emailVerificationCode: code }).catch( + () => { + logger.error('Could not login with emailVerificationCode') + throw new Error('Could not login with emailVerificationCode') + }, + ) + logger.debug('userContact loaded...') // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes - if (!isOptInValid(optInCode)) { + if (!isEmailVerificationCodeValid(userContact.updatedAt)) { logger.error( `email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`, ) @@ -628,7 +669,7 @@ export class UserResolver { logger.debug('optInCode is valid...') // load user - const user = await DbUser.findOneOrFail({ id: optInCode.userId }).catch(() => { + const user = await DbUser.findOneOrFail({ id: userContact.userId }).catch(() => { logger.error('Could not find corresponding Login User') throw new Error('Could not find corresponding Login User') }) @@ -652,10 +693,10 @@ export class UserResolver { logger.debug('Passphrase is valid...') // Activate EMail - user.emailChecked = true + userContact.emailChecked = true // Update Password - const passwordHash = SecretKeyCryptographyCreateKey(user.email, password) // return short and long hash + const passwordHash = SecretKeyCryptographyCreateKey(userContact.email, password) // return short and long hash const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1]) user.password = passwordHash[0].readBigUInt64LE() // using the shorthash @@ -686,11 +727,11 @@ export class UserResolver { // Sign into Klicktipp // TODO do we always signUp the user? How to handle things with old users? - if (optInCode.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) { + if (userContact.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) { try { - await klicktippSignIn(user.email, user.language, user.firstName, user.lastName) + await klicktippSignIn(userContact.email, user.language, user.firstName, user.lastName) logger.debug( - `klicktippSignIn(${user.email}, ${user.language}, ${user.firstName}, ${user.lastName})`, + `klicktippSignIn(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`, ) } catch (e) { logger.error('Error subscribe to klicktipp:' + e) @@ -709,10 +750,10 @@ export class UserResolver { @Query(() => Boolean) async queryOptIn(@Arg('optIn') optIn: string): Promise { logger.info(`queryOptIn(${optIn})...`) - const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: optIn }) - logger.debug(`found optInCode=${optInCode}`) + const userContact = await DbUserContact.findOneOrFail({ emailVerificationCode: optIn }) + logger.debug(`found optInCode=${userContact}`) // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes - if (!isOptInValid(optInCode)) { + if (!isEmailVerificationCodeValid(userContact.updatedAt)) { logger.error( `email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`, ) @@ -760,7 +801,10 @@ export class UserResolver { } // TODO: This had some error cases defined - like missing private key. This is no longer checked. - const oldPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, password) + const oldPasswordHash = SecretKeyCryptographyCreateKey( + userEntity.emailContact.email, + password, + ) if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) { logger.error(`Old password is invalid`) throw new Error(`Old password is invalid`) @@ -768,7 +812,10 @@ export class UserResolver { const privKey = SecretKeyCryptographyDecrypt(userEntity.privKey, oldPasswordHash[1]) logger.debug('oldPassword decrypted...') - const newPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, passwordNew) // return short and long hash + const newPasswordHash = SecretKeyCryptographyCreateKey( + userEntity.emailContact.email, + passwordNew, + ) // return short and long hash logger.debug('newPasswordHash created...') const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1]) logger.debug('PrivateKey encrypted...') @@ -813,6 +860,36 @@ export class UserResolver { logger.debug(`has ElopageBuys = ${elopageBuys}`) return elopageBuys } + + @Authorized([RIGHTS.SEARCH_ADMIN_USERS]) + @Query(() => SearchAdminUsersResult) + async searchAdminUsers( + @Args() + { currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated, + ): Promise { + const userRepository = getCustomRepository(UserRepository) + + const [users, count] = await userRepository.findAndCount({ + where: { + isAdmin: Not(IsNull()), + }, + order: { + createdAt: order, + }, + skip: (currentPage - 1) * pageSize, + take: pageSize, + }) + + return { + userCount: count, + userList: users.map((user) => { + return { + firstName: user.firstName, + lastName: user.lastName, + } + }), + } + } } async function findUserByEmail(email: string): Promise { @@ -847,7 +924,10 @@ const isOptInValid = (optIn: LoginEmailOptIn): boolean => { return isTimeExpired(optIn, CONFIG.EMAIL_CODE_VALID_TIME) } */ -const isEmailVerificationCodeValid = (updatedAt: Date): boolean => { +const isEmailVerificationCodeValid = (updatedAt: Date | null): boolean => { + if (updatedAt == null) { + return true + } return isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_VALID_TIME) } /* @@ -875,4 +955,3 @@ export const printTimeDuration = (duration: number): string => { if (time.hours) return `${time.hours} hours` + (result !== '' ? ` and ${result}` : '') return result } - diff --git a/backend/src/middleware/klicktippMiddleware.ts b/backend/src/middleware/klicktippMiddleware.ts index b3699f29b..6bdaa63fd 100644 --- a/backend/src/middleware/klicktippMiddleware.ts +++ b/backend/src/middleware/klicktippMiddleware.ts @@ -2,6 +2,7 @@ import { MiddlewareFn } from 'type-graphql' import { /* klicktippSignIn, */ getKlickTippUser } from '@/apis/KlicktippController' import { KlickTipp } from '@model/KlickTipp' import CONFIG from '@/config' +import { klickTippLogger as logger } from '@/server/logger' // export const klicktippRegistrationMiddleware: MiddlewareFn = async ( // // Only for demo @@ -29,7 +30,9 @@ export const klicktippNewsletterStateMiddleware: MiddlewareFn = async ( if (klickTippUser) { klickTipp = new KlickTipp(klickTippUser) } - } catch (err) {} + } catch (err) { + logger.error(`There is no user for (email='${result.email}') ${err}`) + } } result.klickTipp = klickTipp return result diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 9f7a02e70..3bd042ac2 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -280,3 +280,15 @@ export const listContributionLinks = gql` } } ` + +export const searchAdminUsers = gql` + query { + searchAdminUsers { + userCount + userList { + firstName + lastName + } + } + } +` diff --git a/backend/src/server/logger.ts b/backend/src/server/logger.ts index cbc8c9b9b..0cfa5689b 100644 --- a/backend/src/server/logger.ts +++ b/backend/src/server/logger.ts @@ -12,7 +12,8 @@ log4js.configure(options) const apolloLogger = log4js.getLogger('apollo') const backendLogger = log4js.getLogger('backend') +const klickTippLogger = log4js.getLogger('klicktipp') backendLogger.addContext('user', 'unknown') -export { apolloLogger, backendLogger } +export { apolloLogger, backendLogger, klickTippLogger } diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index 65dee6728..45fb6d4fb 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -1,47 +1,49 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ - -import { SaveOptions, RemoveOptions } from '@dbTools/typeorm' -import { User as dbUser } from '@entity/User' -// import { UserContact as EmailContact } from '@entity/UserContact' -import { User } from '@model/User' - -const communityDbUser: dbUser = { - id: -1, - gradidoID: '11111111-2222-3333-4444-55555555', - alias: '', - // email: 'support@gradido.net', - firstName: 'Gradido', - lastName: 'Akademie', - pubKey: Buffer.from(''), - privKey: Buffer.from(''), - deletedAt: null, - password: BigInt(0), - // emailHash: Buffer.from(''), - createdAt: new Date(), - // emailChecked: false, - language: '', - isAdmin: null, - publisherId: 0, - passphrase: '', - hasId: function (): boolean { - throw new Error('Function not implemented.') - }, - save: function (options?: SaveOptions): Promise { - throw new Error('Function not implemented.') - }, - remove: function (options?: RemoveOptions): Promise { - throw new Error('Function not implemented.') - }, - softRemove: function (options?: SaveOptions): Promise { - throw new Error('Function not implemented.') - }, - recover: function (options?: SaveOptions): Promise { - throw new Error('Function not implemented.') - }, - reload: function (): Promise { - throw new Error('Function not implemented.') - }, -} -const communityUser = new User(communityDbUser) - -export { communityDbUser, communityUser } +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import { SaveOptions, RemoveOptions } from '@dbTools/typeorm' +import { User as dbUser } from '@entity/User' +import { UserContact } from '@entity/UserContact' +// import { UserContact as EmailContact } from '@entity/UserContact' +import { User } from '@model/User' + +const communityDbUser: dbUser = { + id: -1, + gradidoID: '11111111-2222-4333-4444-55555555', + alias: '', + // email: 'support@gradido.net', + emailContact: new UserContact(), + firstName: 'Gradido', + lastName: 'Akademie', + pubKey: Buffer.from(''), + privKey: Buffer.from(''), + deletedAt: null, + password: BigInt(0), + // emailHash: Buffer.from(''), + createdAt: new Date(), + // emailChecked: false, + language: '', + isAdmin: null, + publisherId: 0, + passphrase: '', + hasId: function (): boolean { + throw new Error('Function not implemented.') + }, + save: function (options?: SaveOptions): Promise { + throw new Error('Function not implemented.') + }, + remove: function (options?: RemoveOptions): Promise { + throw new Error('Function not implemented.') + }, + softRemove: function (options?: SaveOptions): Promise { + throw new Error('Function not implemented.') + }, + recover: function (options?: SaveOptions): Promise { + throw new Error('Function not implemented.') + }, + reload: function (): Promise { + throw new Error('Function not implemented.') + }, +} +const communityUser = new User(communityDbUser) + +export { communityDbUser, communityUser } diff --git a/backend/yarn.lock b/backend/yarn.lock index 731404d6d..dd84e2ce5 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -5442,7 +5442,7 @@ uuid@^3.1.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.0.0: +uuid@^8.0.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== diff --git a/database/entity/0045-add_denied_type_and_status_to_contributions/Contribution.ts b/database/entity/0045-add_denied_type_and_status_to_contributions/Contribution.ts new file mode 100644 index 000000000..c376ae53e --- /dev/null +++ b/database/entity/0045-add_denied_type_and_status_to_contributions/Contribution.ts @@ -0,0 +1,83 @@ +import Decimal from 'decimal.js-light' +import { + BaseEntity, + Column, + Entity, + PrimaryGeneratedColumn, + DeleteDateColumn, + JoinColumn, + ManyToOne, +} from 'typeorm' +import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' +import { User } from '../User' + +@Entity('contributions') +export class Contribution extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ unsigned: true, nullable: false, name: 'user_id' }) + userId: number + + @ManyToOne(() => User, (user) => user.contributions) + @JoinColumn({ name: 'user_id' }) + user: User + + @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' }) + createdAt: Date + + @Column({ type: 'datetime', nullable: false, name: 'contribution_date' }) + contributionDate: 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({ unsigned: true, nullable: true, name: 'moderator_id' }) + moderatorId: number + + @Column({ unsigned: true, nullable: true, name: 'contribution_link_id' }) + contributionLinkId: number + + @Column({ unsigned: true, nullable: true, name: 'confirmed_by' }) + confirmedBy: number + + @Column({ nullable: true, name: 'confirmed_at' }) + confirmedAt: Date + + @Column({ unsigned: true, nullable: true, name: 'denied_by' }) + deniedBy: number + + @Column({ nullable: true, name: 'denied_at' }) + deniedAt: Date + + @Column({ + name: 'contribution_type', + length: 12, + nullable: false, + collation: 'utf8mb4_unicode_ci', + }) + contributionType: string + + @Column({ + name: 'contribution_status', + length: 12, + nullable: false, + collation: 'utf8mb4_unicode_ci', + }) + contributionStatus: string + + @Column({ unsigned: true, nullable: true, name: 'transaction_id' }) + transactionId: number + + @DeleteDateColumn({ name: 'deleted_at' }) + deletedAt: Date | null +} diff --git a/database/entity/0046-adapt_users_table_for_gradidoid/User.ts b/database/entity/0046-adapt_users_table_for_gradidoid/User.ts new file mode 100644 index 000000000..3f2547cad --- /dev/null +++ b/database/entity/0046-adapt_users_table_for_gradidoid/User.ts @@ -0,0 +1,111 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + DeleteDateColumn, + OneToMany, + JoinColumn, +} from 'typeorm' +import { Contribution } from '../Contribution' + +@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) +export class User extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ + name: 'gradido_id', + length: 36, + nullable: false, + unique: true, + collation: 'utf8mb4_unicode_ci', + }) + gradidoID: string + + @Column({ + name: 'alias', + length: 20, + nullable: true, + unique: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + alias: string + + @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: 'contribution_link_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + contributionLinkId?: 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(() => Contribution, (contribution) => contribution.user) + @JoinColumn({ name: 'user_id' }) + contributions?: Contribution[] +} diff --git a/database/entity/0045-adapt_users_table_for_gradidoid/User.ts b/database/entity/0047-add_user_contacts_table/User.ts similarity index 99% rename from database/entity/0045-adapt_users_table_for_gradidoid/User.ts rename to database/entity/0047-add_user_contacts_table/User.ts index 69a085a87..010cb0c20 100644 --- a/database/entity/0045-adapt_users_table_for_gradidoid/User.ts +++ b/database/entity/0047-add_user_contacts_table/User.ts @@ -47,10 +47,8 @@ export class User extends BaseEntity { @JoinColumn({ name: 'email_id' }) emailContact: UserContact - /* @Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null }) emailId?: number | null - */ @Column({ name: 'first_name', diff --git a/database/entity/0045-adapt_users_table_for_gradidoid/UserContact.ts b/database/entity/0047-add_user_contacts_table/UserContact.ts similarity index 100% rename from database/entity/0045-adapt_users_table_for_gradidoid/UserContact.ts rename to database/entity/0047-add_user_contacts_table/UserContact.ts diff --git a/database/entity/Contribution.ts b/database/entity/Contribution.ts index 82dd6478c..800e7f9cd 100644 --- a/database/entity/Contribution.ts +++ b/database/entity/Contribution.ts @@ -1 +1 @@ -export { Contribution } from './0039-contributions_table/Contribution' +export { Contribution } from './0045-add_denied_type_and_status_to_contributions/Contribution' diff --git a/database/entity/User.ts b/database/entity/User.ts index 89b5d3d7f..1e0017b72 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0045-adapt_users_table_for_gradidoid/User' +export { User } from './0047-add_user_contacts_table/User' diff --git a/database/entity/UserContact.ts b/database/entity/UserContact.ts index ac47fac24..e596489da 100644 --- a/database/entity/UserContact.ts +++ b/database/entity/UserContact.ts @@ -1 +1 @@ -export { UserContact } from './0045-adapt_users_table_for_gradidoid/UserContact' +export { UserContact } from './0047-add_user_contacts_table/UserContact' diff --git a/database/migrations/0045-add_denied_type_and_status_to_contributions.ts b/database/migrations/0045-add_denied_type_and_status_to_contributions.ts new file mode 100644 index 000000000..b3653589b --- /dev/null +++ b/database/migrations/0045-add_denied_type_and_status_to_contributions.ts @@ -0,0 +1,39 @@ +/* MIGRATION TO ADD denied_by, denied_at, contribution_type and contrinution_status +FIELDS TO contributions */ + +/* 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 `contributions` ADD COLUMN `denied_at` datetime DEFAULT NULL AFTER `confirmed_at`;', + ) + await queryFn( + 'ALTER TABLE `contributions` ADD COLUMN `denied_by` int(10) unsigned DEFAULT NULL AFTER `denied_at`;', + ) + await queryFn( + 'ALTER TABLE `contributions` ADD COLUMN `contribution_type` varchar(12) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT "ADMIN" AFTER `denied_by`;', + ) + await queryFn( + 'ALTER TABLE `contributions` ADD COLUMN `contribution_status` varchar(12) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT "PENDING" AFTER `contribution_type`;', + ) + await queryFn( + 'UPDATE `contributions` SET `contribution_type` = "LINK" WHERE `contribution_link_id` IS NOT NULL;', + ) + await queryFn( + 'UPDATE `contributions` SET `contribution_type` = "USER" WHERE `contribution_link_id` IS NULL AND `moderator_id` IS NULL;', + ) + await queryFn( + 'UPDATE `contributions` SET `contribution_status` = "CONFIRMED" WHERE `confirmed_at` IS NOT NULL;', + ) + await queryFn( + 'UPDATE `contributions` SET `contribution_status` = "DELETED" WHERE `deleted_at` IS NOT NULL;', + ) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('ALTER TABLE `contributions` DROP COLUMN `contribution_status`;') + await queryFn('ALTER TABLE `contributions` DROP COLUMN `contribution_type`;') + await queryFn('ALTER TABLE `contributions` DROP COLUMN `denied_by`;') + await queryFn('ALTER TABLE `contributions` DROP COLUMN `denied_at`;') +} diff --git a/database/migrations/0046-adapt_users_table_for_gradidoid.ts b/database/migrations/0046-adapt_users_table_for_gradidoid.ts new file mode 100644 index 000000000..8e8372efa --- /dev/null +++ b/database/migrations/0046-adapt_users_table_for_gradidoid.ts @@ -0,0 +1,44 @@ +/* MIGRATION TO ADD GRADIDO_ID + * + * This migration adds new columns to the table `users` and creates the + * new table `user_contacts` + */ + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { v4 as uuidv4 } from 'uuid' + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + // First add gradido_id as nullable column without Default + await queryFn('ALTER TABLE `users` ADD COLUMN `gradido_id` CHAR(36) NULL AFTER `id`;') + + // Second update gradido_id with ensured unique uuidv4 + const usersToUpdate = await queryFn('SELECT `id`, `gradido_id` FROM `users`') // WHERE 'u.gradido_id' is null`,) + for (const id in usersToUpdate) { + const user = usersToUpdate[id] + let gradidoId = null + let countIds = null + do { + gradidoId = uuidv4() + countIds = await queryFn( + `SELECT COUNT(*) FROM \`users\` WHERE \`gradido_id\` = "${gradidoId}"`, + ) + } while (countIds[0] > 0) + await queryFn( + `UPDATE \`users\` SET \`gradido_id\` = "${gradidoId}" WHERE \`id\` = "${user.id}"`, + ) + } + + // third modify gradido_id to not nullable and unique + await queryFn('ALTER TABLE `users` MODIFY COLUMN `gradido_id` CHAR(36) NOT NULL UNIQUE;') + + await queryFn( + 'ALTER TABLE `users` ADD COLUMN `alias` varchar(20) NULL UNIQUE AFTER `gradido_id`;', + ) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('ALTER TABLE users DROP COLUMN gradido_id;') + await queryFn('ALTER TABLE users DROP COLUMN alias;') +} diff --git a/database/migrations/0045-adapt_users_table_for_gradidoid.ts b/database/migrations/0047-add_user_contacts_table.ts similarity index 74% rename from database/migrations/0045-adapt_users_table_for_gradidoid.ts rename to database/migrations/0047-add_user_contacts_table.ts index 8f3f83f28..b3c6be03e 100644 --- a/database/migrations/0045-adapt_users_table_for_gradidoid.ts +++ b/database/migrations/0047-add_user_contacts_table.ts @@ -27,33 +27,6 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis PRIMARY KEY (\`id\`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`) - // First add gradido_id as nullable column without Default - await queryFn('ALTER TABLE `users` ADD COLUMN `gradido_id` CHAR(36) NULL AFTER `id`;') - - // Second update gradido_id with ensured unique uuidv4 - const usersToUpdate = await queryFn('SELECT `id`, `gradido_id` FROM `users`') // WHERE 'u.gradido_id' is null`,) - for (const id in usersToUpdate) { - const user = usersToUpdate[id] - let gradidoId = null - let countIds = null - do { - gradidoId = uuidv4() - countIds = await queryFn( - `SELECT COUNT(*) FROM \`users\` WHERE \`gradido_id\` = "${gradidoId}"`, - ) - } while (countIds[0] > 0) - await queryFn( - `UPDATE \`users\` SET \`gradido_id\` = "${gradidoId}" WHERE \`id\` = "${user.id}"`, - ) - } - - // third modify gradido_id to not nullable and unique - await queryFn('ALTER TABLE `users` MODIFY COLUMN `gradido_id` CHAR(36) NOT NULL UNIQUE;') - - await queryFn( - 'ALTER TABLE `users` ADD COLUMN `alias` varchar(20) NULL UNIQUE AFTER `gradido_id`;', - ) - await queryFn('ALTER TABLE `users` ADD COLUMN `email_id` int(10) NULL AFTER `email`;') // merge values from login_email_opt_in table with users.email in new user_contacts table @@ -104,7 +77,5 @@ export async function downgrade(queryFn: (query: string, values?: any[]) => Prom // write downgrade logic as parameter of queryFn await queryFn(`DROP TABLE IF EXISTS user_contacts;`) - await queryFn('ALTER TABLE users DROP COLUMN gradido_id;') - await queryFn('ALTER TABLE users DROP COLUMN alias;') await queryFn('ALTER TABLE users DROP COLUMN email_id;') } diff --git a/database/package.json b/database/package.json index 4e3591bbb..837e61438 100644 --- a/database/package.json +++ b/database/package.json @@ -1,6 +1,6 @@ { "name": "gradido-database", - "version": "1.10.1", + "version": "1.11.0", "description": "Gradido Database Tool to execute database migrations", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/database", diff --git a/docu/Locales/GRADIDO_register_page_spanish.xlsx b/docu/Locales/GRADIDO_register_page_spanish.xlsx new file mode 100644 index 000000000..6b6ec70c0 Binary files /dev/null and b/docu/Locales/GRADIDO_register_page_spanish.xlsx differ diff --git a/frontend/jest.config.js b/frontend/jest.config.js index 2a52ec707..a32330f3b 100644 --- a/frontend/jest.config.js +++ b/frontend/jest.config.js @@ -23,5 +23,5 @@ module.exports = { testMatch: ['**/?(*.)+(spec|test).js?(x)'], // snapshotSerializers: ['jest-serializer-vue'], transformIgnorePatterns: ['/node_modules/(?!vee-validate/dist/rules)'], - testEnvironment: 'jest-environment-jsdom-sixteen', + // testEnvironment: 'jest-environment-jsdom-sixteen', // not needed anymore since jest@26, see: https://www.npmjs.com/package/jest-environment-jsdom-sixteen } diff --git a/frontend/package.json b/frontend/package.json index f51dd8266..8121e2ca2 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "bootstrap-vue-gradido-wallet", - "version": "1.10.1", + "version": "1.11.0", "private": true, "scripts": { "start": "node run/server.js", @@ -44,7 +44,6 @@ "identity-obj-proxy": "^3.0.0", "jest": "^26.6.3", "jest-canvas-mock": "^2.3.1", - "jest-environment-jsdom-sixteen": "^2.0.0", "jwt-decode": "^3.1.2", "portal-vue": "^2.1.7", "prettier": "^2.2.1", diff --git a/frontend/src/components/ClipboardCopy.vue b/frontend/src/components/ClipboardCopy.vue index 7a6cf0ec1..ebab31aad 100644 --- a/frontend/src/components/ClipboardCopy.vue +++ b/frontend/src/components/ClipboardCopy.vue @@ -3,8 +3,11 @@ - - {{ $t('gdd_per_link.copy') }} + + {{ $t('gdd_per_link.copy-link-with-text') }} + + + {{ $t('gdd_per_link.copy-link') }} @@ -18,29 +21,10 @@