Merge branch 'master' into 2892-Frontend-multiple-auto-logout-messages

This commit is contained in:
Alexander Friedland 2023-05-31 11:39:46 +02:00 committed by GitHub
commit 72f101ecc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
133 changed files with 2406 additions and 838 deletions

View File

@ -4,8 +4,49 @@ 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). Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [1.21.0](https://github.com/gradido/gradido/compare/1.20.0...1.21.0)
- feat(frontend): preserve email after login [`#2994`](https://github.com/gradido/gradido/pull/2994)
- feat(frontend): send coins via identifier [`#2989`](https://github.com/gradido/gradido/pull/2989)
- feat(backend): export user events to klicktipp [`#2916`](https://github.com/gradido/gradido/pull/2916)
- fix(backend): add extension pug json and css to nodemon. [`#2996`](https://github.com/gradido/gradido/pull/2996)
- feat(backend): send coins via alias [`#2988`](https://github.com/gradido/gradido/pull/2988)
- refactor(backend): replace jasonwebtoken with jose [`#2975`](https://github.com/gradido/gradido/pull/2975)
- feat(frontend): username in wallet [`#2984`](https://github.com/gradido/gradido/pull/2984)
- feat(frontend): add community to send form [`#2986`](https://github.com/gradido/gradido/pull/2986)
- fix(frontend): date fns locales [`#2983`](https://github.com/gradido/gradido/pull/2983)
- refactor(federation): federation reduce spam [`#2967`](https://github.com/gradido/gradido/pull/2967)
- refactor(federation): refactor federation clients [`#2965`](https://github.com/gradido/gradido/pull/2965)
- feat(backend): migrate transactions table for x community sendcoins [`#2917`](https://github.com/gradido/gradido/pull/2917)
- feat(backend): alias in update user info [`#2727`](https://github.com/gradido/gradido/pull/2727)
- refactor(backend): eslint comments [`#2981`](https://github.com/gradido/gradido/pull/2981)
- refactor(backend): eslint security [`#2980`](https://github.com/gradido/gradido/pull/2980)
- refactor(backend): rename klicktippSignIn to subscribe. [`#2973`](https://github.com/gradido/gradido/pull/2973)
- refactor(backend): eslint typescript strict [`#2979`](https://github.com/gradido/gradido/pull/2979)
- fix(frontend): between store problems [`#2972`](https://github.com/gradido/gradido/pull/2972)
- refactor(other): delete build folders [`#2977`](https://github.com/gradido/gradido/pull/2977)
- refactor(backend): no email in user [`#2953`](https://github.com/gradido/gradido/pull/2953)
- refactor(frontend): remove email in wallet [`#2952`](https://github.com/gradido/gradido/pull/2952)
- fix(frontend): update jest-canvas-mock version to resolve window mock problem in tests [`#2974`](https://github.com/gradido/gradido/pull/2974)
- feat(federation): federation autoreload on codechange [`#2969`](https://github.com/gradido/gradido/pull/2969)
- feat(backend): add fields to subscriber [`#2887`](https://github.com/gradido/gradido/pull/2887)
- feat(backend): x-com-2: distingue communities and communities_federation in database [`#2890`](https://github.com/gradido/gradido/pull/2890)
- feat(backend): add event for subscribe and unsubscribe [`#2886`](https://github.com/gradido/gradido/pull/2886)
- refactor(backend): eslint disable more typesafety [`#2922`](https://github.com/gradido/gradido/pull/2922)
- refactor(backend): eslint disable tests typesafer [`#2921`](https://github.com/gradido/gradido/pull/2921)
- refactor(backend): eslint disable @typescript eslint/unbound method [`#2920`](https://github.com/gradido/gradido/pull/2920)
- docs(other): removed obsolete yarn cron docu [`#2909`](https://github.com/gradido/gradido/pull/2909)
- refactor(other): finalize workflow separation and resolve mariadb and database dependencies in workflow files [`#2962`](https://github.com/gradido/gradido/pull/2962)
- refactor(workflow): align workflow naming and remove docker-compose filter from build tests [`#2894`](https://github.com/gradido/gradido/pull/2894)
- refactor(backend): eslint plugin promise + fixes [`#2830`](https://github.com/gradido/gradido/pull/2830)
- fix(backend): log stack trace included [`#2915`](https://github.com/gradido/gradido/pull/2915)
- refactor(backend): prettier refine config [`#2832`](https://github.com/gradido/gradido/pull/2832)
#### [1.20.0](https://github.com/gradido/gradido/compare/1.19.1...1.20.0) #### [1.20.0](https://github.com/gradido/gradido/compare/1.19.1...1.20.0)
> 12 April 2023
- chore(release): v1.20.0 [`#2939`](https://github.com/gradido/gradido/pull/2939)
- fix(backend): no await for emails [`#2918`](https://github.com/gradido/gradido/pull/2918) - fix(backend): no await for emails [`#2918`](https://github.com/gradido/gradido/pull/2918)
- fix(frontend): no receiver on send by link [`#2933`](https://github.com/gradido/gradido/pull/2933) - fix(frontend): no receiver on send by link [`#2933`](https://github.com/gradido/gradido/pull/2933)
- fix(admin): pagination set currentPage by switch tabs [`#2902`](https://github.com/gradido/gradido/pull/2902) - fix(admin): pagination set currentPage by switch tabs [`#2902`](https://github.com/gradido/gradido/pull/2902)

View File

@ -1,3 +1,3 @@
node_modules/ node_modules/
dist/ build/
coverage/ coverage/

2
admin/.gitignore vendored
View File

@ -1,5 +1,5 @@
node_modules/ node_modules/
dist/ build/
.cache/ .cache/
/.env /.env

View File

@ -84,7 +84,7 @@ CMD /bin/sh -c "yarn run dev"
FROM base as production FROM base as production
# Copy "binary"-files from build image # Copy "binary"-files from build image
COPY --from=build ${DOCKER_WORKDIR}/dist ./dist COPY --from=build ${DOCKER_WORKDIR}/build ./build
# We also copy the node_modules express and serve-static for the run script # We also copy the node_modules express and serve-static for the run script
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
# Copy static files # Copy static files

View File

@ -3,7 +3,7 @@
"description": "Administraion Interface for Gradido", "description": "Administraion Interface for Gradido",
"main": "index.js", "main": "index.js",
"author": "Moriz Wahl", "author": "Moriz Wahl",
"version": "1.20.0", "version": "1.21.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"private": false, "private": false,
"scripts": { "scripts": {
@ -11,7 +11,7 @@
"serve": "vue-cli-service serve --open", "serve": "vue-cli-service serve --open",
"build": "vue-cli-service build", "build": "vue-cli-service build",
"dev": "yarn run serve", "dev": "yarn run serve",
"analyse-bundle": "yarn build && webpack-bundle-analyzer dist/webpack.stats.json", "analyse-bundle": "yarn build && webpack-bundle-analyzer build/webpack.stats.json",
"lint": "eslint --max-warnings=0 --ext .js,.vue,.json .", "lint": "eslint --max-warnings=0 --ext .js,.vue,.json .",
"stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'", "stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'",
"test": "cross-env TZ=UTC jest", "test": "cross-env TZ=UTC jest",

View File

@ -9,10 +9,10 @@ const port = process.env.PORT || 8080
// Express Server // Express Server
const app = express() const app = express()
// Serve files // Serve files
app.use(express.static(path.join(__dirname, '../dist'))) app.use(express.static(path.join(__dirname, '../build')))
// Default to index.html // Default to index.html
app.get('*', (req, res) => { app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../dist/index.html')) res.sendFile(path.join(__dirname, '../build/index.html'))
}) })
app.listen(port, hostname, () => { app.listen(port, hostname, () => {

View File

@ -13,7 +13,7 @@
</template> </template>
<script> <script>
import { formatDistanceToNow } from 'date-fns' import { formatDistanceToNow } from 'date-fns'
import { de, en, fr, es, nl } from 'date-fns/locale' import { de, enUS as en, fr, es, nl } from 'date-fns/locale'
const locales = { en, de, es, fr, nl } const locales = { en, de, es, fr, nl }

View File

@ -37,6 +37,7 @@ export const actions = {
const store = new Vuex.Store({ const store = new Vuex.Store({
plugins: [ plugins: [
createPersistedState({ createPersistedState({
key: 'gradido-admin',
storage: window.localStorage, storage: window.localStorage,
}), }),
], ],

View File

@ -49,5 +49,5 @@ module.exports = {
// Enable CSS source maps. // Enable CSS source maps.
sourceMap: CONFIG.NODE_ENV !== 'production', sourceMap: CONFIG.NODE_ENV !== 'production',
}, },
outputDir: path.resolve(__dirname, './dist'), outputDir: path.resolve(__dirname, './build'),
} }

View File

@ -1,3 +1,4 @@
node_modules node_modules
**/*.min.js **/*.min.js
build build
coverage

View File

@ -12,6 +12,8 @@ module.exports = {
'plugin:prettier/recommended', 'plugin:prettier/recommended',
'plugin:import/recommended', 'plugin:import/recommended',
'plugin:import/typescript', 'plugin:import/typescript',
'plugin:security/recommended',
'plugin:@eslint-community/eslint-comments/recommended',
], ],
settings: { settings: {
'import/parsers': { 'import/parsers': {
@ -25,7 +27,8 @@ module.exports = {
}, },
}, },
rules: { rules: {
'no-console': ['error'], 'no-console': 'error',
camelcase: ['error', { allow: ['FederationClient_*'] }],
'no-debugger': 'error', 'no-debugger': 'error',
'prettier/prettier': [ 'prettier/prettier': [
'error', 'error',
@ -151,6 +154,11 @@ module.exports = {
'promise/valid-params': 'warn', 'promise/valid-params': 'warn',
'promise/prefer-await-to-callbacks': 'error', 'promise/prefer-await-to-callbacks': 'error',
'promise/no-multiple-resolved': 'error', 'promise/no-multiple-resolved': 'error',
// eslint comments
'@eslint-community/eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }],
'@eslint-community/eslint-comments/no-restricted-disable': 'error',
'@eslint-community/eslint-comments/no-use': 'off',
'@eslint-community/eslint-comments/require-description': 'off',
}, },
overrides: [ overrides: [
// only for ts files // only for ts files
@ -159,6 +167,7 @@ module.exports = {
extends: [ extends: [
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking', 'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:@typescript-eslint/strict',
'plugin:type-graphql/recommended', 'plugin:type-graphql/recommended',
], ],
rules: { rules: {
@ -169,11 +178,14 @@ module.exports = {
'@typescript-eslint/prefer-regexp-exec': 'off', '@typescript-eslint/prefer-regexp-exec': 'off',
// this should not run on ts files: https://github.com/import-js/eslint-plugin-import/issues/2215#issuecomment-911245486 // this should not run on ts files: https://github.com/import-js/eslint-plugin-import/issues/2215#issuecomment-911245486
'import/unambiguous': 'off', 'import/unambiguous': 'off',
// this is not compatible with typeorm, due to joined tables can be null, but are not defined as nullable
'@typescript-eslint/no-unnecessary-condition': 'off',
}, },
parserOptions: { parserOptions: {
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
project: ['./tsconfig.json', '**/tsconfig.json'], project: ['./tsconfig.json', '**/tsconfig.json'],
// this is to properly reference the referenced project database without requirement of compiling it // this is to properly reference the referenced project database without requirement of compiling it
// eslint-disable-next-line camelcase
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true, EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
}, },
}, },

View File

@ -7,7 +7,7 @@ module.exports = {
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
coverageThreshold: { coverageThreshold: {
global: { global: {
lines: 85, lines: 89,
}, },
}, },
setupFiles: ['<rootDir>/test/testSetup.ts'], setupFiles: ['<rootDir>/test/testSetup.ts'],

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido-backend", "name": "gradido-backend",
"version": "1.20.0", "version": "1.21.0",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions", "description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts", "main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend", "repository": "https://github.com/gradido/gradido/backend",
@ -11,11 +11,11 @@
"build": "tsc --build && mkdir -p build/src/emails/templates/ && cp -r src/emails/templates/* build/src/emails/templates/ && mkdir -p build/src/locales/ && cp -r src/locales/*.json build/src/locales/", "build": "tsc --build && mkdir -p build/src/emails/templates/ && cp -r src/emails/templates/* build/src/emails/templates/ && mkdir -p build/src/locales/ && cp -r src/locales/*.json build/src/locales/",
"clean": "tsc --build --clean", "clean": "tsc --build --clean",
"start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js", "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", "dev": "cross-env TZ=UTC nodemon -w src --ext ts,pug,json,css --exec ts-node -r tsconfig-paths/register src/index.ts",
"lint": "eslint --max-warnings=0 .", "lint": "eslint --max-warnings=0 .",
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --forceExit --detectOpenHandles", "test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --forceExit --detectOpenHandles",
"seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts", "seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts",
"klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/klicktipp.ts", "klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/executeKlicktipp.ts",
"locales": "scripts/sort.sh" "locales": "scripts/sort.sh"
}, },
"dependencies": { "dependencies": {
@ -33,7 +33,7 @@
"graphql": "^15.5.1", "graphql": "^15.5.1",
"graphql-request": "5.0.0", "graphql-request": "5.0.0",
"i18n": "^0.15.1", "i18n": "^0.15.1",
"jsonwebtoken": "^8.5.1", "jose": "^4.14.4",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"log4js": "^6.4.6", "log4js": "^6.4.6",
"mysql2": "^2.3.0", "mysql2": "^2.3.0",
@ -46,12 +46,12 @@
"uuid": "^8.3.2" "uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "^3.2.1",
"@types/email-templates": "^10.0.1", "@types/email-templates": "^10.0.1",
"@types/express": "^4.17.12", "@types/express": "^4.17.12",
"@types/faker": "^5.5.9", "@types/faker": "^5.5.9",
"@types/i18n": "^0.13.4", "@types/i18n": "^0.13.4",
"@types/jest": "^27.0.2", "@types/jest": "^27.0.2",
"@types/jsonwebtoken": "^8.5.2",
"@types/lodash.clonedeep": "^4.5.6", "@types/lodash.clonedeep": "^4.5.6",
"@types/node": "^16.10.3", "@types/node": "^16.10.3",
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "^6.4.4",
@ -68,6 +68,7 @@
"eslint-plugin-n": "^15.7.0", "eslint-plugin-n": "^15.7.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^6.1.1", "eslint-plugin-promise": "^6.1.1",
"eslint-plugin-security": "^1.7.1",
"eslint-plugin-type-graphql": "^1.0.0", "eslint-plugin-type-graphql": "^1.0.0",
"faker": "^5.5.3", "faker": "^5.5.3",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",

View File

@ -7,7 +7,6 @@ import axios from 'axios'
import { LogError } from '@/server/LogError' import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const apiPost = async (url: string, payload: unknown): Promise<any> => { export const apiPost = async (url: string, payload: unknown): Promise<any> => {
logger.trace('POST', url, payload) logger.trace('POST', url, payload)
try { try {
@ -25,7 +24,6 @@ export const apiPost = async (url: string, payload: unknown): Promise<any> => {
} }
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const apiGet = async (url: string): Promise<any> => { export const apiGet = async (url: string): Promise<any> => {
logger.trace('GET: url=' + url) logger.trace('GET: url=' + url)
try { try {

View File

@ -4,15 +4,15 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { CONFIG } from '@/config' import { CONFIG } from '@/config'
import { backendLogger as logger } from '@/server/logger'
// eslint-disable-next-line import/no-relative-parent-imports // eslint-disable-next-line import/no-relative-parent-imports
import KlicktippConnector from 'klicktipp-api' import KlicktippConnector from 'klicktipp-api'
const klicktippConnector = new KlicktippConnector() const klicktippConnector = new KlicktippConnector()
export const klicktippSignIn = async ( export const subscribe = async (
email: string, email: string,
language: string, language: string,
firstName?: string, firstName?: string,
@ -28,13 +28,6 @@ export const klicktippSignIn = async (
return result return result
} }
export const signout = async (email: string, language: string): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
const apiKey = language === 'de' ? CONFIG.KLICKTIPP_APIKEY_DE : CONFIG.KLICKTIPP_APIKEY_EN
const result = await klicktippConnector.signoff(apiKey, email)
return result
}
export const unsubscribe = async (email: string): Promise<boolean> => { export const unsubscribe = async (email: string): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser() const isLogin = await loginKlicktippUser()
@ -48,9 +41,12 @@ export const getKlickTippUser = async (email: string): Promise<any> => {
if (!CONFIG.KLICKTIPP) return true if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser() const isLogin = await loginKlicktippUser()
if (isLogin) { if (isLogin) {
const subscriberId = await klicktippConnector.subscriberSearch(email) try {
const result = await klicktippConnector.subscriberGet(subscriberId) return klicktippConnector.subscriberGet(await klicktippConnector.subscriberSearch(email))
return result } catch (e) {
logger.error('Could not find subscriber', email)
return false
}
} }
return false return false
} }
@ -60,38 +56,6 @@ export const loginKlicktippUser = async (): Promise<boolean> => {
return await klicktippConnector.login(CONFIG.KLICKTIPP_USER, CONFIG.KLICKTIPP_PASSWORD) return await klicktippConnector.login(CONFIG.KLICKTIPP_USER, CONFIG.KLICKTIPP_PASSWORD)
} }
export const logoutKlicktippUser = async (): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
return await klicktippConnector.logout()
}
export const untagUser = async (email: string, tagId: string): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser()
if (isLogin) {
return await klicktippConnector.untag(email, tagId)
}
return false
}
export const tagUser = async (email: string, tagIds: string): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser()
if (isLogin) {
return await klicktippConnector.tag(email, tagIds)
}
return false
}
export const getKlicktippTagMap = async () => {
if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser()
if (isLogin) {
return await klicktippConnector.tagIndex()
}
return ''
}
export const addFieldsToSubscriber = async ( export const addFieldsToSubscriber = async (
email: string, email: string,
fields: any = {}, fields: any = {},
@ -101,8 +65,18 @@ export const addFieldsToSubscriber = async (
if (!CONFIG.KLICKTIPP) return true if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser() const isLogin = await loginKlicktippUser()
if (isLogin) { if (isLogin) {
const subscriberId = await klicktippConnector.subscriberSearch(email) try {
return klicktippConnector.subscriberUpdate(subscriberId, fields, newemail, newsmsnumber) logger.info('Updating of subscriber', email)
return klicktippConnector.subscriberUpdate(
await klicktippConnector.subscriberSearch(email),
fields,
newemail,
newsmsnumber,
)
} catch (e) {
logger.error('Could not update subscriber', email, fields, e)
return false
}
} }
return false return false
} }

View File

@ -1,5 +1,5 @@
import { JwtPayload } from 'jsonwebtoken' import { JWTPayload } from 'jose'
export interface CustomJwtPayload extends JwtPayload { export interface CustomJwtPayload extends JWTPayload {
gradidoID: string gradidoID: string
} }

View File

@ -8,4 +8,5 @@ export const INALIENABLE_RIGHTS = [
RIGHTS.SET_PASSWORD, RIGHTS.SET_PASSWORD,
RIGHTS.QUERY_TRANSACTION_LINK, RIGHTS.QUERY_TRANSACTION_LINK,
RIGHTS.QUERY_OPT_IN, RIGHTS.QUERY_OPT_IN,
RIGHTS.CHECK_USERNAME,
] ]

View File

@ -1,22 +1,33 @@
import { verify, sign } from 'jsonwebtoken' import { SignJWT, jwtVerify } from 'jose'
import { CONFIG } from '@/config/' import { CONFIG } from '@/config/'
import { LogError } from '@/server/LogError' import { LogError } from '@/server/LogError'
import { CustomJwtPayload } from './CustomJwtPayload' import { CustomJwtPayload } from './CustomJwtPayload'
export const decode = (token: string): CustomJwtPayload | null => { export const decode = async (token: string): Promise<CustomJwtPayload | null> => {
if (!token) throw new LogError('401 Unauthorized') if (!token) throw new LogError('401 Unauthorized')
try { try {
return <CustomJwtPayload>verify(token, CONFIG.JWT_SECRET) const secret = new TextEncoder().encode(CONFIG.JWT_SECRET)
const { payload } = await jwtVerify(token, secret, {
issuer: 'urn:gradido:issuer',
audience: 'urn:gradido:audience',
})
return payload as CustomJwtPayload
} catch (err) { } catch (err) {
return null return null
} }
} }
export const encode = (gradidoID: string): string => { export const encode = async (gradidoID: string): Promise<string> => {
const token = sign({ gradidoID }, CONFIG.JWT_SECRET, { const secret = new TextEncoder().encode(CONFIG.JWT_SECRET)
expiresIn: CONFIG.JWT_EXPIRES_IN, const token = await new SignJWT({ gradidoID, 'urn:gradido:claim': true })
}) .setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setIssuer('urn:gradido:issuer')
.setAudience('urn:gradido:audience')
.setExpirationTime(CONFIG.JWT_EXPIRES_IN)
.sign(secret)
return token return token
} }

View File

@ -34,6 +34,7 @@ export enum RIGHTS {
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES', LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
OPEN_CREATIONS = 'OPEN_CREATIONS', OPEN_CREATIONS = 'OPEN_CREATIONS',
USER = 'USER', USER = 'USER',
CHECK_USERNAME = 'CHECK_USERNAME',
// Admin // Admin
SEARCH_USERS = 'SEARCH_USERS', SEARCH_USERS = 'SEARCH_USERS',
SET_USER_ROLE = 'SET_USER_ROLE', SET_USER_ROLE = 'SET_USER_ROLE',

View File

@ -12,11 +12,11 @@ Decimal.set({
}) })
const constants = { const constants = {
DB_VERSION: '0065-refactor_communities_table', DB_VERSION: '0066-x-community-sendcoins-transactions_table',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json', LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info // default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info', LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
CONFIG_VERSION: { CONFIG_VERSION: {
DEFAULT: 'DEFAULT', DEFAULT: 'DEFAULT',
EXPECTED: 'v15.2023-02-07', EXPECTED: 'v15.2023-02-07',
@ -25,67 +25,67 @@ const constants = {
} }
const server = { const server = {
PORT: process.env.PORT || 4000, PORT: process.env.PORT ?? 4000,
JWT_SECRET: process.env.JWT_SECRET || 'secret123', JWT_SECRET: process.env.JWT_SECRET ?? 'secret123',
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m', JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN ?? '10m',
GRAPHIQL: process.env.GRAPHIQL === 'true' || false, GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
GDT_API_URL: process.env.GDT_API_URL || 'https://gdt.gradido.net', GDT_API_URL: process.env.GDT_API_URL ?? 'https://gdt.gradido.net',
PRODUCTION: process.env.NODE_ENV === 'production' || false, PRODUCTION: process.env.NODE_ENV === 'production' || false,
} }
const database = { const database = {
DB_HOST: process.env.DB_HOST || 'localhost', DB_HOST: process.env.DB_HOST ?? 'localhost',
DB_PORT: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306, DB_PORT: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306,
DB_USER: process.env.DB_USER || 'root', DB_USER: process.env.DB_USER ?? 'root',
DB_PASSWORD: process.env.DB_PASSWORD || '', DB_PASSWORD: process.env.DB_PASSWORD ?? '',
DB_DATABASE: process.env.DB_DATABASE || 'gradido_community', DB_DATABASE: process.env.DB_DATABASE ?? 'gradido_community',
TYPEORM_LOGGING_RELATIVE_PATH: process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.backend.log', TYPEORM_LOGGING_RELATIVE_PATH: process.env.TYPEORM_LOGGING_RELATIVE_PATH ?? 'typeorm.backend.log',
} }
const klicktipp = { const klicktipp = {
KLICKTIPP: process.env.KLICKTIPP === 'true' || false, KLICKTIPP: process.env.KLICKTIPP === 'true' || false,
KLICKTTIPP_API_URL: process.env.KLICKTIPP_API_URL || 'https://api.klicktipp.com', KLICKTTIPP_API_URL: process.env.KLICKTIPP_API_URL ?? 'https://api.klicktipp.com',
KLICKTIPP_USER: process.env.KLICKTIPP_USER || 'gradido_test', KLICKTIPP_USER: process.env.KLICKTIPP_USER ?? 'gradido_test',
KLICKTIPP_PASSWORD: process.env.KLICKTIPP_PASSWORD || 'secret321', KLICKTIPP_PASSWORD: process.env.KLICKTIPP_PASSWORD ?? 'secret321',
KLICKTIPP_APIKEY_DE: process.env.KLICKTIPP_APIKEY_DE || 'SomeFakeKeyDE', KLICKTIPP_APIKEY_DE: process.env.KLICKTIPP_APIKEY_DE ?? 'SomeFakeKeyDE',
KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN || 'SomeFakeKeyEN', KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN ?? 'SomeFakeKeyEN',
} }
const community = { const community = {
COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung', COMMUNITY_NAME: process.env.COMMUNITY_NAME ?? 'Gradido Entwicklung',
COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/', COMMUNITY_URL: process.env.COMMUNITY_URL ?? 'http://localhost/',
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register', COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL ?? 'http://localhost/register',
COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL || 'http://localhost/redeem/{code}', COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL ?? 'http://localhost/redeem/{code}',
COMMUNITY_REDEEM_CONTRIBUTION_URL: COMMUNITY_REDEEM_CONTRIBUTION_URL:
process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}', process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL ?? 'http://localhost/redeem/CL-{code}',
COMMUNITY_DESCRIPTION: COMMUNITY_DESCRIPTION:
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.', process.env.COMMUNITY_DESCRIPTION ?? 'Die lokale Entwicklungsumgebung von Gradido.',
COMMUNITY_SUPPORT_MAIL: process.env.COMMUNITY_SUPPORT_MAIL || 'support@supportmail.com', COMMUNITY_SUPPORT_MAIL: process.env.COMMUNITY_SUPPORT_MAIL ?? 'support@supportmail.com',
} }
const loginServer = { const loginServer = {
LOGIN_APP_SECRET: process.env.LOGIN_APP_SECRET || '21ffbbc616fe', LOGIN_APP_SECRET: process.env.LOGIN_APP_SECRET ?? '21ffbbc616fe',
LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY || 'a51ef8ac7ef1abf162fb7a65261acd7a', LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY ?? 'a51ef8ac7ef1abf162fb7a65261acd7a',
} }
const email = { const email = {
EMAIL: process.env.EMAIL === 'true' || false, EMAIL: process.env.EMAIL === 'true' || false,
EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' || false, EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' || false,
EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER || 'stage1@gradido.net', EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER ?? 'stage1@gradido.net',
EMAIL_USERNAME: process.env.EMAIL_USERNAME || '', EMAIL_USERNAME: process.env.EMAIL_USERNAME ?? '',
EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net', EMAIL_SENDER: process.env.EMAIL_SENDER ?? 'info@gradido.net',
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || '', EMAIL_PASSWORD: process.env.EMAIL_PASSWORD ?? '',
EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'mailserver', EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL ?? 'mailserver',
EMAIL_SMTP_PORT: Number(process.env.EMAIL_SMTP_PORT) || 1025, EMAIL_SMTP_PORT: Number(process.env.EMAIL_SMTP_PORT) || 1025,
// eslint-disable-next-line no-unneeded-ternary // eslint-disable-next-line no-unneeded-ternary
EMAIL_TLS: process.env.EMAIL_TLS === 'false' ? false : true, EMAIL_TLS: process.env.EMAIL_TLS === 'false' ? false : true,
EMAIL_LINK_VERIFICATION: EMAIL_LINK_VERIFICATION:
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{optin}{code}', process.env.EMAIL_LINK_VERIFICATION ?? 'http://localhost/checkEmail/{optin}{code}',
EMAIL_LINK_SETPASSWORD: EMAIL_LINK_SETPASSWORD:
process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{optin}', process.env.EMAIL_LINK_SETPASSWORD ?? 'http://localhost/reset-password/{optin}',
EMAIL_LINK_FORGOTPASSWORD: EMAIL_LINK_FORGOTPASSWORD:
process.env.EMAIL_LINK_FORGOTPASSWORD || 'http://localhost/forgot-password', process.env.EMAIL_LINK_FORGOTPASSWORD ?? 'http://localhost/forgot-password',
EMAIL_LINK_OVERVIEW: process.env.EMAIL_LINK_OVERVIEW || 'http://localhost/overview', EMAIL_LINK_OVERVIEW: process.env.EMAIL_LINK_OVERVIEW ?? 'http://localhost/overview',
// time in minutes a optin code is valid // time in minutes a optin code is valid
EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME
? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 1440 ? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 1440
@ -98,14 +98,14 @@ const email = {
const webhook = { const webhook = {
// Elopage // Elopage
WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret', WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET ?? 'secret',
} }
// This is needed by graphql-directive-auth // This is needed by graphql-directive-auth
process.env.APP_SECRET = server.JWT_SECRET process.env.APP_SECRET = server.JWT_SECRET
// Check config version // Check config version
constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION ?? constants.CONFIG_VERSION.DEFAULT
if ( if (
![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes( ![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes(
constants.CONFIG_VERSION.CURRENT, constants.CONFIG_VERSION.CURRENT,

View File

@ -1,44 +1,49 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { gql } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import { GraphQLGetClient } from '@/federation/client/GraphQLGetClient' import { getPublicKey } from '@/federation/client/1_0/query/getPublicKey'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
export async function requestGetPublicKey( // eslint-disable-next-line camelcase
dbCom: DbFederatedCommunity, export class FederationClient {
): Promise<string | undefined> { dbCom: DbFederatedCommunity
let endpoint = dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/' endpoint: string
endpoint = `${endpoint}${dbCom.apiVersion}/` client: GraphQLClient
logger.info(`requestGetPublicKey with endpoint='${endpoint}'...`)
const graphQLClient = GraphQLGetClient.getInstance(endpoint) constructor(dbCom: DbFederatedCommunity) {
logger.debug(`graphQLClient=${JSON.stringify(graphQLClient)}`) this.dbCom = dbCom
const query = gql` this.endpoint = `${dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'}${
query { dbCom.apiVersion
getPublicKey { }/`
publicKey this.client = new GraphQLClient(this.endpoint, {
method: 'GET',
jsonSerializer: {
parse: JSON.parse,
stringify: JSON.stringify,
},
})
} }
}
`
const variables = {}
getPublicKey = async (): Promise<string | undefined> => {
logger.info('Federation: getPublicKey from endpoint', this.endpoint)
try { try {
const { data, errors, extensions, headers, status } = await graphQLClient.rawRequest( // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
query, const { data } = await this.client.rawRequest(getPublicKey, {})
variables, // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
) if (!data?.getPublicKey?.publicKey) {
logger.debug(`Response-Data:`, data, errors, extensions, headers, status) logger.warn('Federation: getPublicKey without response data from endpoint', this.endpoint)
if (data) { return
logger.debug(`Response-PublicKey:`, data.getPublicKey.publicKey)
logger.info(`requestGetPublicKey processed successfully`)
return data.getPublicKey.publicKey
} }
logger.warn(`requestGetPublicKey processed without response data`) logger.info(
'Federation: getPublicKey successful from endpoint',
this.endpoint,
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
data.getPublicKey.publicKey,
)
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
return data.getPublicKey.publicKey
} catch (err) { } catch (err) {
throw new LogError(`Request-Error:`, err) logger.warn('Federation: getPublicKey failed for endpoint', this.endpoint)
}
} }
} }

View File

@ -0,0 +1,9 @@
import { gql } from 'graphql-request'
export const getPublicKey = gql`
query {
getPublicKey {
publicKey
}
}
`

View File

@ -1,44 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */ // eslint-disable-next-line camelcase
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient'
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { gql } from 'graphql-request'
import { GraphQLGetClient } from '@/federation/client/GraphQLGetClient' // eslint-disable-next-line camelcase
import { LogError } from '@/server/LogError' export class FederationClient extends V1_0_FederationClient {}
import { backendLogger as logger } from '@/server/logger'
export async function requestGetPublicKey(
dbCom: DbFederatedCommunity,
): Promise<string | undefined> {
let endpoint = dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'
endpoint = `${endpoint}${dbCom.apiVersion}/`
logger.info(`requestGetPublicKey with endpoint='${endpoint}'...`)
const graphQLClient = GraphQLGetClient.getInstance(endpoint)
logger.debug(`graphQLClient=${JSON.stringify(graphQLClient)}`)
const query = gql`
query {
getPublicKey {
publicKey
}
}
`
const variables = {}
try {
const { data, errors, extensions, headers, status } = await graphQLClient.rawRequest(
query,
variables,
)
logger.debug(`Response-Data:`, data, errors, extensions, headers, status)
if (data) {
logger.debug(`Response-PublicKey:`, data.getPublicKey.publicKey)
logger.info(`requestGetPublicKey processed successfully`)
return data.getPublicKey.publicKey
}
logger.warn(`requestGetPublicKey processed without response data`)
} catch (err) {
throw new LogError(`Request-Error:`, err)
}
}

View File

@ -0,0 +1,62 @@
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
// eslint-disable-next-line camelcase
import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient'
// eslint-disable-next-line camelcase
import { FederationClient as V1_1_FederationClient } from '@/federation/client/1_1/FederationClient'
import { ApiVersionType } from '@/federation/enum/apiVersionType'
// eslint-disable-next-line camelcase
type FederationClient = V1_0_FederationClient | V1_1_FederationClient
interface FederationClientInstance {
id: number
// eslint-disable-next-line no-use-before-define
client: FederationClient
}
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class FederationClientFactory {
private static instanceArray: FederationClientInstance[] = []
/**
* The Singleton's constructor should always be private to prevent direct
* construction calls with the `new` operator.
*/
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
private constructor() {}
private static createFederationClient = (dbCom: DbFederatedCommunity) => {
switch (dbCom.apiVersion) {
case ApiVersionType.V1_0:
return new V1_0_FederationClient(dbCom)
case ApiVersionType.V1_1:
return new V1_1_FederationClient(dbCom)
default:
return null
}
}
/**
* The static method that controls the access to the singleton instance.
*
* This implementation let you subclass the Singleton class while keeping
* just one instance of each subclass around.
*/
public static getInstance(dbCom: DbFederatedCommunity): FederationClient | null {
const instance = FederationClientFactory.instanceArray.find(
(instance) => instance.id === dbCom.id,
)
if (instance) {
return instance.client
}
const client = FederationClientFactory.createFederationClient(dbCom)
if (client) {
FederationClientFactory.instanceArray.push({
id: dbCom.id,
client,
} as FederationClientInstance)
}
return client
}
}

View File

@ -1,43 +0,0 @@
import { GraphQLClient } from 'graphql-request'
import { PatchedRequestInit } from 'graphql-request/dist/types'
type ClientInstance = {
url: string
// eslint-disable-next-line no-use-before-define
client: GraphQLGetClient
}
export class GraphQLGetClient extends GraphQLClient {
private static instanceArray: ClientInstance[] = []
/**
* The Singleton's constructor should always be private to prevent direct
* construction calls with the `new` operator.
*/
// eslint-disable-next-line no-useless-constructor
private constructor(url: string, options?: PatchedRequestInit) {
super(url, options)
}
/**
* The static method that controls the access to the singleton instance.
*
* This implementation let you subclass the Singleton class while keeping
* just one instance of each subclass around.
*/
public static getInstance(url: string): GraphQLGetClient {
const instance = GraphQLGetClient.instanceArray.find((instance) => instance.url === url)
if (instance) {
return instance.client
}
const client = new GraphQLGetClient(url, {
method: 'GET',
jsonSerializer: {
parse: JSON.parse,
stringify: JSON.stringify,
},
})
GraphQLGetClient.instanceArray.push({ url, client } as ClientInstance)
return client
}
}

View File

@ -8,6 +8,8 @@
import { Connection } from '@dbTools/typeorm' import { Connection } from '@dbTools/typeorm'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { ApolloServerTestClient } from 'apollo-server-testing' import { ApolloServerTestClient } from 'apollo-server-testing'
import { GraphQLClient } from 'graphql-request'
import { Response } from 'graphql-request/dist/types'
import { testEnvironment, cleanDB } from '@test/helpers' import { testEnvironment, cleanDB } from '@test/helpers'
import { logger } from '@test/testSetup' import { logger } from '@test/testSetup'
@ -57,10 +59,23 @@ describe('validate Communities', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 0 dbCommunities`) expect(logger.debug).toBeCalledWith(`Federation: found 0 dbCommunities`)
}) })
describe('with one Community of api 1_0', () => { describe('with one Community of api 1_0 and not matching pubKey', () => {
beforeEach(async () => { beforeEach(async () => {
// eslint-disable-next-line @typescript-eslint/require-await
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return {
data: {
getPublicKey: {
publicKey: 'somePubKey',
},
},
} as Response<unknown>
})
const variables1 = { const variables1 = {
publicKey: Buffer.from('11111111111111111111111111111111'), publicKey: Buffer.from(
'1111111111111111111111111111111111111111111111111111111111111111',
),
apiVersion: '1_0', apiVersion: '1_0',
endPoint: 'http//localhost:5001/api/', endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(), lastAnnouncedAt: new Date(),
@ -70,6 +85,7 @@ describe('validate Communities', () => {
.into(DbFederatedCommunity) .into(DbFederatedCommunity)
.values(variables1) .values(variables1)
.orUpdate({ .orUpdate({
// eslint-disable-next-line camelcase
conflict_target: ['id', 'publicKey', 'apiVersion'], conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'], overwrite: ['end_point', 'last_announced_at'],
}) })
@ -84,14 +100,89 @@ describe('validate Communities', () => {
}) })
it('logs requestGetPublicKey for community api 1_0 ', () => { it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith( expect(logger.info).toBeCalledWith(
`requestGetPublicKey with endpoint='http//localhost:5001/api/1_0/'...`, 'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_0/',
)
})
it('logs not matching publicKeys', () => {
expect(logger.warn).toBeCalledWith(
'Federation: received not matching publicKey:',
'somePubKey',
expect.stringMatching('1111111111111111111111111111111111111111111111111111111111111111'),
)
})
})
describe('with one Community of api 1_0 and matching pubKey', () => {
beforeEach(async () => {
// eslint-disable-next-line @typescript-eslint/require-await
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return {
data: {
getPublicKey: {
publicKey: '1111111111111111111111111111111111111111111111111111111111111111',
},
},
} as Response<unknown>
})
const variables1 = {
publicKey: Buffer.from(
'1111111111111111111111111111111111111111111111111111111111111111',
),
apiVersion: '1_0',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbFederatedCommunity.createQueryBuilder()
.insert()
.into(DbFederatedCommunity)
.values(variables1)
.orUpdate({
// eslint-disable-next-line camelcase
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
await DbFederatedCommunity.update({}, { verifiedAt: null })
jest.clearAllMocks()
await validateCommunities()
})
it('logs one community found', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`)
})
it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith(
'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_0/',
)
})
it('logs community pubKey verified', () => {
expect(logger.info).toHaveBeenNthCalledWith(
3,
'Federation: verified community with',
'http//localhost:5001/api/',
) )
}) })
}) })
describe('with two Communities of api 1_0 and 1_1', () => { describe('with two Communities of api 1_0 and 1_1', () => {
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks()
// eslint-disable-next-line @typescript-eslint/require-await
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return {
data: {
getPublicKey: {
publicKey: '1111111111111111111111111111111111111111111111111111111111111111',
},
},
} as Response<unknown>
})
const variables2 = { const variables2 = {
publicKey: Buffer.from('11111111111111111111111111111111'), publicKey: Buffer.from(
'1111111111111111111111111111111111111111111111111111111111111111',
),
apiVersion: '1_1', apiVersion: '1_1',
endPoint: 'http//localhost:5001/api/', endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(), lastAnnouncedAt: new Date(),
@ -101,11 +192,13 @@ describe('validate Communities', () => {
.into(DbFederatedCommunity) .into(DbFederatedCommunity)
.values(variables2) .values(variables2)
.orUpdate({ .orUpdate({
// eslint-disable-next-line camelcase
conflict_target: ['id', 'publicKey', 'apiVersion'], conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'], overwrite: ['end_point', 'last_announced_at'],
}) })
.execute() .execute()
await DbFederatedCommunity.update({}, { verifiedAt: null })
jest.clearAllMocks() jest.clearAllMocks()
await validateCommunities() await validateCommunities()
}) })
@ -114,12 +207,14 @@ describe('validate Communities', () => {
}) })
it('logs requestGetPublicKey for community api 1_0 ', () => { it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith( expect(logger.info).toBeCalledWith(
`requestGetPublicKey with endpoint='http//localhost:5001/api/1_0/'...`, 'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_0/',
) )
}) })
it('logs requestGetPublicKey for community api 1_1 ', () => { it('logs requestGetPublicKey for community api 1_1 ', () => {
expect(logger.info).toBeCalledWith( expect(logger.info).toBeCalledWith(
`requestGetPublicKey with endpoint='http//localhost:5001/api/1_1/'...`, 'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_1/',
) )
}) })
}) })
@ -127,7 +222,9 @@ describe('validate Communities', () => {
let dbCom: DbFederatedCommunity let dbCom: DbFederatedCommunity
beforeEach(async () => { beforeEach(async () => {
const variables3 = { const variables3 = {
publicKey: Buffer.from('11111111111111111111111111111111'), publicKey: Buffer.from(
'1111111111111111111111111111111111111111111111111111111111111111',
),
apiVersion: '2_0', apiVersion: '2_0',
endPoint: 'http//localhost:5001/api/', endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(), lastAnnouncedAt: new Date(),
@ -137,6 +234,7 @@ describe('validate Communities', () => {
.into(DbFederatedCommunity) .into(DbFederatedCommunity)
.values(variables3) .values(variables3)
.orUpdate({ .orUpdate({
// eslint-disable-next-line camelcase
conflict_target: ['id', 'publicKey', 'apiVersion'], conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'], overwrite: ['end_point', 'last_announced_at'],
}) })
@ -144,6 +242,7 @@ describe('validate Communities', () => {
dbCom = await DbFederatedCommunity.findOneOrFail({ dbCom = await DbFederatedCommunity.findOneOrFail({
where: { publicKey: variables3.publicKey, apiVersion: variables3.apiVersion }, where: { publicKey: variables3.publicKey, apiVersion: variables3.apiVersion },
}) })
await DbFederatedCommunity.update({}, { verifiedAt: null })
jest.clearAllMocks() jest.clearAllMocks()
await validateCommunities() await validateCommunities()
}) })
@ -152,18 +251,21 @@ describe('validate Communities', () => {
}) })
it('logs requestGetPublicKey for community api 1_0 ', () => { it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith( expect(logger.info).toBeCalledWith(
`requestGetPublicKey with endpoint='http//localhost:5001/api/1_0/'...`, 'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_0/',
) )
}) })
it('logs requestGetPublicKey for community api 1_1 ', () => { it('logs requestGetPublicKey for community api 1_1 ', () => {
expect(logger.info).toBeCalledWith( expect(logger.info).toBeCalledWith(
`requestGetPublicKey with endpoint='http//localhost:5001/api/1_1/'...`, 'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_1/',
) )
}) })
it('logs unsupported api for community with api 2_0 ', () => { it('logs unsupported api for community with api 2_0 ', () => {
expect(logger.warn).toBeCalledWith( expect(logger.warn).toBeCalledWith(
`Federation: dbCom: ${dbCom.id} with unsupported apiVersion=2_0; supported versions`, 'Federation: dbCom with unsupported apiVersion',
['1_0', '1_1'], dbCom.endPoint,
'2_0',
) )
}) })
}) })

View File

@ -3,13 +3,11 @@
import { IsNull } from '@dbTools/typeorm' import { IsNull } from '@dbTools/typeorm'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { LogError } from '@/server/LogError' // eslint-disable-next-line camelcase
import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient'
import { FederationClientFactory } from '@/federation/client/FederationClientFactory'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
// eslint-disable-next-line camelcase
import { requestGetPublicKey as v1_0_requestGetPublicKey } from './client/1_0/FederationClient'
// eslint-disable-next-line camelcase
import { requestGetPublicKey as v1_1_requestGetPublicKey } from './client/1_1/FederationClient'
import { ApiVersionType } from './enum/apiVersionType' import { ApiVersionType } from './enum/apiVersionType'
export function startValidateCommunities(timerInterval: number): void { export function startValidateCommunities(timerInterval: number): void {
@ -36,56 +34,28 @@ export async function validateCommunities(): Promise<void> {
logger.debug('Federation: dbCom', dbCom) logger.debug('Federation: dbCom', dbCom)
const apiValueStrings: string[] = Object.values(ApiVersionType) const apiValueStrings: string[] = Object.values(ApiVersionType)
logger.debug(`suppported ApiVersions=`, apiValueStrings) logger.debug(`suppported ApiVersions=`, apiValueStrings)
if (apiValueStrings.includes(dbCom.apiVersion)) { if (!apiValueStrings.includes(dbCom.apiVersion)) {
logger.debug( logger.warn('Federation: dbCom with unsupported apiVersion', dbCom.endPoint, dbCom.apiVersion)
`Federation: validate publicKey for dbCom: ${dbCom.id} with apiVersion=${dbCom.apiVersion}`, continue
) }
try { try {
const pubKey = await invokeVersionedRequestGetPublicKey(dbCom) const client = FederationClientFactory.getInstance(dbCom)
logger.info( // eslint-disable-next-line camelcase
'Federation: received publicKey from endpoint', if (client instanceof V1_0_FederationClient) {
pubKey, const pubKey = await client.getPublicKey()
`${dbCom.endPoint}/${dbCom.apiVersion}`,
)
if (pubKey && pubKey === dbCom.publicKey.toString()) { if (pubKey && pubKey === dbCom.publicKey.toString()) {
logger.info(`Federation: matching publicKey: ${pubKey}`)
await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() }) await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() })
logger.debug(`Federation: updated dbCom: ${JSON.stringify(dbCom)}`) logger.info('Federation: verified community with', dbCom.endPoint)
} else { } else {
logger.warn( logger.warn(
`Federation: received not matching publicKey -> received: ${ 'Federation: received not matching publicKey:',
pubKey || 'null' pubKey,
}, expected: ${dbCom.publicKey.toString()} `, dbCom.publicKey.toString(),
) )
// DbCommunity.delete({ id: dbCom.id }) }
} }
} catch (err) { } catch (err) {
if (!isLogError(err)) {
logger.error(`Error:`, err) logger.error(`Error:`, err)
} }
} }
} else {
logger.warn(
`Federation: dbCom: ${dbCom.id} with unsupported apiVersion=${dbCom.apiVersion}; supported versions`,
apiValueStrings,
)
}
}
}
function isLogError(err: unknown) {
return err instanceof LogError
}
async function invokeVersionedRequestGetPublicKey(
dbCom: DbFederatedCommunity,
): Promise<string | undefined> {
switch (dbCom.apiVersion) {
case ApiVersionType.V1_0:
return v1_0_requestGetPublicKey(dbCom)
case ApiVersionType.V1_1:
return v1_1_requestGetPublicKey(dbCom)
default:
return undefined
}
} }

View File

@ -8,6 +8,9 @@ export class UpdateUserInfosArgs {
@Field({ nullable: true }) @Field({ nullable: true })
lastName?: string lastName?: string
@Field({ nullable: true })
alias?: string
@Field({ nullable: true }) @Field({ nullable: true })
language?: string language?: string

View File

@ -12,7 +12,7 @@ export const isAuthorized: AuthChecker<Context> = async ({ context }, rights) =>
context.role = ROLE_UNAUTHORIZED // unauthorized user context.role = ROLE_UNAUTHORIZED // unauthorized user
// is rights an inalienable right? // is rights an inalienable right?
if ((<RIGHTS[]>rights).reduce((acc, right) => acc && INALIENABLE_RIGHTS.includes(right), true)) if ((rights as RIGHTS[]).reduce((acc, right) => acc && INALIENABLE_RIGHTS.includes(right), true))
return true return true
// Do we have a token? // Do we have a token?
@ -21,7 +21,7 @@ export const isAuthorized: AuthChecker<Context> = async ({ context }, rights) =>
} }
// Decode the token // Decode the token
const decoded = decode(context.token) const decoded = await decode(context.token)
if (!decoded) { if (!decoded) {
throw new LogError('403.13 - Client certificate revoked') throw new LogError('403.13 - Client certificate revoked')
} }
@ -43,12 +43,12 @@ export const isAuthorized: AuthChecker<Context> = async ({ context }, rights) =>
} }
// check for correct rights // check for correct rights
const missingRights = (<RIGHTS[]>rights).filter((right) => !context.role?.hasRight(right)) const missingRights = (rights as RIGHTS[]).filter((right) => !context.role?.hasRight(right))
if (missingRights.length !== 0) { if (missingRights.length !== 0) {
throw new LogError('401 Unauthorized') throw new LogError('401 Unauthorized')
} }
// set new header token // set new header token
context.setHeaders.push({ key: 'token', value: encode(decoded.gradidoID) }) context.setHeaders.push({ key: 'token', value: await encode(decoded.gradidoID) })
return true return true
} }

View File

@ -10,7 +10,7 @@ export class Balance {
linkCount: number linkCount: number
}) { }) {
this.balance = data.balance this.balance = data.balance
this.balanceGDT = data.balanceGDT || null this.balanceGDT = data.balanceGDT ?? null
this.count = data.count this.count = data.count
this.linkCount = data.linkCount this.linkCount = data.linkCount
} }

View File

@ -43,13 +43,12 @@ export class Transaction {
this.memo = transaction.memo this.memo = transaction.memo
this.creationDate = transaction.creationDate this.creationDate = transaction.creationDate
this.linkedUser = linkedUser this.linkedUser = linkedUser
this.linkedTransactionId = transaction.linkedTransactionId || null this.linkedTransactionId = transaction.linkedTransactionId ?? null
this.linkId = transaction.contribution this.linkId = transaction.contribution
? transaction.contribution.contributionLinkId ? transaction.contribution.contributionLinkId
: transaction.transactionLinkId || null : transaction.transactionLinkId ?? null
this.previousBalance = this.previousBalance =
(transaction.previousTransaction && transaction.previousTransaction?.balance.toDecimalPlaces(2, Decimal.ROUND_DOWN) ??
transaction.previousTransaction.balance.toDecimalPlaces(2, Decimal.ROUND_DOWN)) ||
new Decimal(0) new Decimal(0)
} }

View File

@ -2,7 +2,6 @@ import { User as dbUser } from '@entity/User'
import { ObjectType, Field, Int } from 'type-graphql' import { ObjectType, Field, Int } from 'type-graphql'
import { KlickTipp } from './KlickTipp' import { KlickTipp } from './KlickTipp'
import { UserContact } from './UserContact'
@ObjectType() @ObjectType()
export class User { export class User {
@ -10,10 +9,7 @@ export class User {
this.id = user.id this.id = user.id
this.gradidoID = user.gradidoID this.gradidoID = user.gradidoID
this.alias = user.alias this.alias = user.alias
this.emailId = user.emailId
if (user.emailContact) { if (user.emailContact) {
this.email = user.emailContact.email
this.emailContact = new UserContact(user.emailContact)
this.emailChecked = user.emailContact.emailChecked this.emailChecked = user.emailContact.emailChecked
} }
this.firstName = user.firstName this.firstName = user.firstName
@ -38,16 +34,6 @@ export class User {
@Field(() => String, { nullable: true }) @Field(() => String, { nullable: true })
alias: string | null alias: string | null
@Field(() => Int, { nullable: true })
emailId: number | null
// TODO privacy issue here
@Field(() => String, { nullable: true })
email: string | null
@Field(() => UserContact)
emailContact: UserContact
@Field(() => String, { nullable: true }) @Field(() => String, { nullable: true })
firstName: string | null firstName: string | null

View File

@ -70,7 +70,10 @@ export class BalanceResolver {
now, now,
) )
logger.info( logger.info(
`calculatedDecay(balance=${lastTransaction.balance}, balanceDate=${lastTransaction.balanceDate})=${calculatedDecay}`, 'calculatedDecay',
lastTransaction.balance,
lastTransaction.balanceDate,
calculatedDecay,
) )
// The final balance is reduced by the link amount withheld // The final balance is reduced by the link amount withheld
@ -96,9 +99,7 @@ export class BalanceResolver {
count, count,
linkCount, linkCount,
}) })
logger.info( logger.info('new Balance', balance, balanceGDT, count, linkCount, newBalance)
`new Balance(balance=${balance}, balanceGDT=${balanceGDT}, count=${count}, linkCount=${linkCount}) = ${newBalance}`,
)
return newBalance return newBalance
} }

View File

@ -66,7 +66,7 @@ let testEnv: {
query: ApolloServerTestClient['query'] query: ApolloServerTestClient['query']
con: Connection con: Connection
} }
let creation: Contribution | void let creation: Contribution | null
let admin: User let admin: User
let pendingContribution: any let pendingContribution: any
let inProgressContribution: any let inProgressContribution: any
@ -2071,7 +2071,7 @@ describe('ContributionResolver', () => {
mutate({ mutate({
mutation: updateContribution, mutation: updateContribution,
variables: { variables: {
contributionId: (adminContribution && adminContribution.id) || -1, contributionId: adminContribution?.id ?? -1,
amount: 100.0, amount: 100.0,
memo: 'Test Test Test', memo: 'Test Test Test',
creationDate: new Date().toString(), creationDate: new Date().toString(),
@ -2565,8 +2565,8 @@ describe('ContributionResolver', () => {
}) })
describe('confirm two creations one after the other quickly', () => { describe('confirm two creations one after the other quickly', () => {
let c1: Contribution | void let c1: Contribution | null
let c2: Contribution | void let c2: Contribution | null
beforeAll(async () => { beforeAll(async () => {
const now = new Date() const now = new Date()

View File

@ -43,6 +43,7 @@ import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
import { calculateDecay } from '@/util/decay' import { calculateDecay } from '@/util/decay'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import { fullName } from '@/util/utilities'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
import { import {
@ -269,7 +270,7 @@ export class ContributionResolver {
withDeleted: true, withDeleted: true,
relations: ['user'], relations: ['user'],
}) })
if (!emailContact || !emailContact.user) { if (!emailContact?.user) {
throw new LogError('Could not find user', email) throw new LogError('Could not find user', email)
} }
if (emailContact.deletedAt || emailContact.user.deletedAt) { if (emailContact.deletedAt || emailContact.user.deletedAt) {
@ -500,6 +501,8 @@ export class ContributionResolver {
transaction.typeId = TransactionTypeId.CREATION transaction.typeId = TransactionTypeId.CREATION
transaction.memo = contribution.memo transaction.memo = contribution.memo
transaction.userId = contribution.userId transaction.userId = contribution.userId
transaction.userGradidoID = user.gradidoID
transaction.userName = fullName(user.firstName, user.lastName)
transaction.previous = lastTransaction ? lastTransaction.id : null transaction.previous = lastTransaction ? lastTransaction.id : null
transaction.amount = contribution.amount transaction.amount = contribution.amount
transaction.creationDate = contribution.contributionDate transaction.creationDate = contribution.contributionDate

View File

@ -1,6 +1,6 @@
import { Resolver, Authorized, Mutation, Ctx } from 'type-graphql' import { Resolver, Authorized, Mutation, Ctx } from 'type-graphql'
import { unsubscribe, klicktippSignIn } from '@/apis/KlicktippController' import { unsubscribe, subscribe } from '@/apis/KlicktippController'
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
import { EVENT_NEWSLETTER_SUBSCRIBE, EVENT_NEWSLETTER_UNSUBSCRIBE } from '@/event/Events' import { EVENT_NEWSLETTER_SUBSCRIBE, EVENT_NEWSLETTER_UNSUBSCRIBE } from '@/event/Events'
import { Context, getUser } from '@/server/context' import { Context, getUser } from '@/server/context'
@ -20,6 +20,6 @@ export class KlicktippResolver {
async subscribeNewsletter(@Ctx() context: Context): Promise<boolean> { async subscribeNewsletter(@Ctx() context: Context): Promise<boolean> {
const user = getUser(context) const user = getUser(context)
await EVENT_NEWSLETTER_SUBSCRIBE(user) await EVENT_NEWSLETTER_SUBSCRIBE(user)
return klicktippSignIn(user.emailContact.email, user.language) return subscribe(user.emailContact.email, user.language)
} }
} }

View File

@ -817,8 +817,8 @@ describe('TransactionLinkResolver', () => {
const bibisTransaktionLinks = transactionLinks.filter( const bibisTransaktionLinks = transactionLinks.filter(
(transactionLink) => transactionLink.email === 'bibi@bloxberg.de', (transactionLink) => transactionLink.email === 'bibi@bloxberg.de',
) )
for (let i = 0; i < bibisTransaktionLinks.length; i++) { for (const bibisTransaktionLink of bibisTransaktionLinks) {
await transactionLinkFactory(testEnv, bibisTransaktionLinks[i]) await transactionLinkFactory(testEnv, bibisTransaktionLink)
} }
// admin: only now log in // admin: only now log in
@ -1040,6 +1040,7 @@ describe('TransactionLinkResolver', () => {
}) })
it('returns a string that ends with the hex value of date', () => { it('returns a string that ends with the hex value of date', () => {
// eslint-disable-next-line security/detect-non-literal-regexp
const regexp = new RegExp(date.getTime().toString(16) + '$') const regexp = new RegExp(date.getTime().toString(16) + '$')
expect(transactionLinkCode(date)).toEqual(expect.stringMatching(regexp)) expect(transactionLinkCode(date)).toEqual(expect.stringMatching(regexp))
}) })

View File

@ -34,6 +34,7 @@ import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
import { calculateDecay } from '@/util/decay' import { calculateDecay } from '@/util/decay'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import { fullName } from '@/util/utilities'
import { calculateBalance } from '@/util/validate' import { calculateBalance } from '@/util/validate'
import { executeTransaction } from './TransactionResolver' import { executeTransaction } from './TransactionResolver'
@ -146,7 +147,7 @@ export class TransactionLinkResolver {
const transactionLink = await DbTransactionLink.findOneOrFail({ code }, { withDeleted: true }) const transactionLink = await DbTransactionLink.findOneOrFail({ code }, { withDeleted: true })
const user = await DbUser.findOneOrFail({ id: transactionLink.userId }) const user = await DbUser.findOneOrFail({ id: transactionLink.userId })
let redeemedBy: User | null = null let redeemedBy: User | null = null
if (transactionLink && transactionLink.redeemedBy) { if (transactionLink?.redeemedBy) {
redeemedBy = new User(await DbUser.findOneOrFail({ id: transactionLink.redeemedBy })) redeemedBy = new User(await DbUser.findOneOrFail({ id: transactionLink.redeemedBy }))
} }
return new TransactionLink(transactionLink, new User(user), redeemedBy) return new TransactionLink(transactionLink, new User(user), redeemedBy)
@ -266,6 +267,8 @@ export class TransactionLinkResolver {
transaction.typeId = TransactionTypeId.CREATION transaction.typeId = TransactionTypeId.CREATION
transaction.memo = contribution.memo transaction.memo = contribution.memo
transaction.userId = contribution.userId transaction.userId = contribution.userId
transaction.userGradidoID = user.gradidoID
transaction.userName = fullName(user.firstName, user.lastName)
transaction.previous = lastTransaction ? lastTransaction.id : null transaction.previous = lastTransaction ? lastTransaction.id : null
transaction.amount = contribution.amount transaction.amount = contribution.amount
transaction.creationDate = contribution.contributionDate transaction.creationDate = contribution.contributionDate

View File

@ -19,13 +19,17 @@ import {
createContribution, createContribution,
login, login,
sendCoins, sendCoins,
updateUserInfos,
} from '@/seeds/graphql/mutations' } from '@/seeds/graphql/mutations'
import { transactionsQuery } from '@/seeds/graphql/queries'
import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { bobBaumeister } from '@/seeds/users/bob-baumeister'
import { garrickOllivander } from '@/seeds/users/garrick-ollivander' import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
import { peterLustig } from '@/seeds/users/peter-lustig' import { peterLustig } from '@/seeds/users/peter-lustig'
import { stephenHawking } from '@/seeds/users/stephen-hawking' import { stephenHawking } from '@/seeds/users/stephen-hawking'
let mutate: ApolloServerTestClient['mutate'], con: Connection let mutate: ApolloServerTestClient['mutate'], con: Connection
let query: ApolloServerTestClient['query']
let testEnv: { let testEnv: {
mutate: ApolloServerTestClient['mutate'] mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query'] query: ApolloServerTestClient['query']
@ -35,6 +39,7 @@ let testEnv: {
beforeAll(async () => { beforeAll(async () => {
testEnv = await testEnvironment(logger) testEnv = await testEnvironment(logger)
mutate = testEnv.mutate mutate = testEnv.mutate
query = testEnv.query
con = testEnv.con con = testEnv.con
await cleanDB() await cleanDB()
}) })
@ -48,10 +53,13 @@ let bobData: any
let peterData: any let peterData: any
let user: User[] let user: User[]
let bob: User
let peter: User
describe('send coins', () => { describe('send coins', () => {
beforeAll(async () => { beforeAll(async () => {
await userFactory(testEnv, peterLustig) peter = await userFactory(testEnv, peterLustig)
await userFactory(testEnv, bobBaumeister) bob = await userFactory(testEnv, bobBaumeister)
await userFactory(testEnv, stephenHawking) await userFactory(testEnv, stephenHawking)
await userFactory(testEnv, garrickOllivander) await userFactory(testEnv, garrickOllivander)
@ -372,6 +380,114 @@ describe('send coins', () => {
}) })
}) })
describe('send coins via gradido ID', () => {
it('sends the coins', async () => {
await expect(
mutate({
mutation: sendCoins,
variables: {
identifier: peter?.gradidoID,
amount: 10,
memo: 'send via gradido ID',
},
}),
).resolves.toMatchObject({
data: {
sendCoins: true,
},
errors: undefined,
})
})
})
describe('send coins via alias', () => {
beforeAll(async () => {
await mutate({
mutation: updateUserInfos,
variables: {
alias: 'bob',
},
})
await mutate({
mutation: login,
variables: peterData,
})
})
afterAll(async () => {
await mutate({
mutation: login,
variables: bobData,
})
})
it('sends the coins', async () => {
await expect(
mutate({
mutation: sendCoins,
variables: {
identifier: 'bob',
amount: 6.66,
memo: 'send via alias',
},
}),
).resolves.toMatchObject({
data: {
sendCoins: true,
},
errors: undefined,
})
})
describe("peter's transactions", () => {
it('has all expected transactions', async () => {
await expect(query({ query: transactionsQuery })).resolves.toMatchObject({
data: {
transactionList: {
balance: expect.any(Object),
transactions: [
expect.objectContaining({
typeId: 'DECAY',
}),
expect.objectContaining({
amount: expect.decimalEqual(-6.66),
linkedUser: {
firstName: 'Bob',
gradidoID: bob?.gradidoID,
lastName: 'der Baumeister',
},
memo: 'send via alias',
typeId: 'SEND',
}),
expect.objectContaining({
amount: expect.decimalEqual(10),
linkedUser: {
firstName: 'Bob',
gradidoID: bob?.gradidoID,
lastName: 'der Baumeister',
},
memo: 'send via gradido ID',
typeId: 'RECEIVE',
}),
expect.objectContaining({
amount: expect.decimalEqual(50),
linkedUser: {
firstName: 'Bob',
gradidoID: bob?.gradidoID,
lastName: 'der Baumeister',
},
memo: 'unrepeatable memo',
typeId: 'RECEIVE',
}),
],
},
},
errors: undefined,
})
})
})
})
describe('more transactions to test semaphore', () => { describe('more transactions to test semaphore', () => {
it('sends the coins four times in a row', async () => { it('sends the coins four times in a row', async () => {
await expect( await expect(
@ -442,3 +558,42 @@ describe('send coins', () => {
}) })
}) })
}) })
describe('transactionList', () => {
describe('unauthenticated', () => {
it('throws an error', async () => {
await expect(query({ query: transactionsQuery })).resolves.toMatchObject({
errors: [new GraphQLError('401 Unauthorized')],
})
})
})
describe('authenticated', () => {
describe('no transactions', () => {
beforeAll(async () => {
await userFactory(testEnv, bobBaumeister)
await mutate({
mutation: login,
variables: {
email: 'bob@baumeister.de',
password: 'Aa12345_',
},
})
})
it('has no transactions and balance 0', async () => {
await expect(query({ query: transactionsQuery })).resolves.toMatchObject({
data: {
transactionList: {
balance: expect.objectContaining({
balance: expect.decimalEqual(0),
}),
transactions: [],
},
},
errors: undefined,
})
})
})
})
})

View File

@ -29,6 +29,7 @@ import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
import { communityUser } from '@/util/communityUser' import { communityUser } from '@/util/communityUser'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import { fullName } from '@/util/utilities'
import { calculateBalance } from '@/util/validate' import { calculateBalance } from '@/util/validate'
import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions' import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions'
@ -48,9 +49,7 @@ export const executeTransaction = async (
// acquire lock // acquire lock
const releaseLock = await TRANSACTIONS_LOCK.acquire() const releaseLock = await TRANSACTIONS_LOCK.acquire()
try { try {
logger.info( logger.info('executeTransaction', amount, memo, sender, recipient)
`executeTransaction(amount=${amount}, memo=${memo}, sender=${sender}, recipient=${recipient})...`,
)
if (sender.id === recipient.id) { if (sender.id === recipient.id) {
throw new LogError('Sender and Recipient are the same', sender.id) throw new LogError('Sender and Recipient are the same', sender.id)
@ -87,7 +86,11 @@ export const executeTransaction = async (
transactionSend.typeId = TransactionTypeId.SEND transactionSend.typeId = TransactionTypeId.SEND
transactionSend.memo = memo transactionSend.memo = memo
transactionSend.userId = sender.id transactionSend.userId = sender.id
transactionSend.userGradidoID = sender.gradidoID
transactionSend.userName = fullName(sender.firstName, sender.lastName)
transactionSend.linkedUserId = recipient.id transactionSend.linkedUserId = recipient.id
transactionSend.linkedUserGradidoID = recipient.gradidoID
transactionSend.linkedUserName = fullName(recipient.firstName, recipient.lastName)
transactionSend.amount = amount.mul(-1) transactionSend.amount = amount.mul(-1)
transactionSend.balance = sendBalance.balance transactionSend.balance = sendBalance.balance
transactionSend.balanceDate = receivedCallDate transactionSend.balanceDate = receivedCallDate
@ -103,7 +106,11 @@ export const executeTransaction = async (
transactionReceive.typeId = TransactionTypeId.RECEIVE transactionReceive.typeId = TransactionTypeId.RECEIVE
transactionReceive.memo = memo transactionReceive.memo = memo
transactionReceive.userId = recipient.id transactionReceive.userId = recipient.id
transactionReceive.userGradidoID = recipient.gradidoID
transactionReceive.userName = fullName(recipient.firstName, recipient.lastName)
transactionReceive.linkedUserId = sender.id transactionReceive.linkedUserId = sender.id
transactionReceive.linkedUserGradidoID = sender.gradidoID
transactionReceive.linkedUserName = fullName(sender.firstName, sender.lastName)
transactionReceive.amount = amount transactionReceive.amount = amount
const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate) const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate)
transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount
@ -119,10 +126,10 @@ export const executeTransaction = async (
// Save linked transaction id for send // Save linked transaction id for send
transactionSend.linkedTransactionId = transactionReceive.id transactionSend.linkedTransactionId = transactionReceive.id
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend) await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
logger.debug(`send Transaction updated: ${transactionSend}`) logger.debug('send Transaction updated', transactionSend)
if (transactionLink) { if (transactionLink) {
logger.info(`transactionLink: ${transactionLink}`) logger.info('transactionLink', transactionLink)
transactionLink.redeemedAt = receivedCallDate transactionLink.redeemedAt = receivedCallDate
transactionLink.redeemedBy = recipient.id transactionLink.redeemedBy = recipient.id
await queryRunner.manager.update( await queryRunner.manager.update(
@ -271,8 +278,8 @@ export class TransactionResolver {
sumAmount.mul(-1), sumAmount.mul(-1),
sumHoldAvailableAmount.mul(-1), sumHoldAvailableAmount.mul(-1),
sumHoldAvailableAmount.minus(sumAmount.toString()).mul(-1), sumHoldAvailableAmount.minus(sumAmount.toString()).mul(-1),
firstDate || now, firstDate ?? now,
lastDate || now, lastDate ?? now,
self, self,
(userTransactions.length && userTransactions[0].balance) || new Decimal(0), (userTransactions.length && userTransactions[0].balance) || new Decimal(0),
), ),
@ -315,7 +322,6 @@ export class TransactionResolver {
throw new LogError('Amount to send must be positive', amount) throw new LogError('Amount to send must be positive', amount)
} }
// TODO this is subject to replay attacks
const senderUser = getUser(context) const senderUser = getUser(context)
// validate recipient user // validate recipient user
@ -325,9 +331,7 @@ export class TransactionResolver {
} }
await executeTransaction(amount, memo, senderUser, recipientUser) await executeTransaction(amount, memo, senderUser, recipientUser)
logger.info( logger.info('successful executeTransaction', amount, memo, senderUser, recipientUser)
`successful executeTransaction(amount=${amount}, memo=${memo}, senderUser=${senderUser}, recipientUser=${recipientUser})`,
)
return true return true
} }
} }

View File

@ -20,6 +20,7 @@ import { ContributionLink } from '@model/ContributionLink'
import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers' import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers'
import { logger, i18n as localization } from '@test/testSetup' import { logger, i18n as localization } from '@test/testSetup'
import { subscribe } from '@/apis/KlicktippController'
import { CONFIG } from '@/config' import { CONFIG } from '@/config'
import { import {
sendAccountActivationEmail, sendAccountActivationEmail,
@ -52,6 +53,7 @@ import {
searchAdminUsers, searchAdminUsers,
searchUsers, searchUsers,
user as userQuery, user as userQuery,
checkUsername,
} from '@/seeds/graphql/queries' } from '@/seeds/graphql/queries'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { bobBaumeister } from '@/seeds/users/bob-baumeister'
@ -61,8 +63,6 @@ import { stephenHawking } from '@/seeds/users/stephen-hawking'
import { printTimeDuration } from '@/util/time' import { printTimeDuration } from '@/util/time'
import { objectValuesToArray } from '@/util/utilities' import { objectValuesToArray } from '@/util/utilities'
// import { klicktippSignIn } from '@/apis/KlicktippController'
jest.mock('@/emails/sendEmailVariants', () => { jest.mock('@/emails/sendEmailVariants', () => {
const originalModule = jest.requireActual('@/emails/sendEmailVariants') const originalModule = jest.requireActual('@/emails/sendEmailVariants')
return { return {
@ -76,15 +76,13 @@ jest.mock('@/emails/sendEmailVariants', () => {
} }
}) })
/*
jest.mock('@/apis/KlicktippController', () => { jest.mock('@/apis/KlicktippController', () => {
return { return {
__esModule: true, __esModule: true,
klicktippSignIn: jest.fn(), subscribe: jest.fn(),
getKlickTippUser: jest.fn(),
} }
}) })
*/
let admin: User let admin: User
let user: User let user: User
@ -556,16 +554,14 @@ describe('UserResolver', () => {
expect(newUser.password.toString()).toEqual(encryptedPass.toString()) expect(newUser.password.toString()).toEqual(encryptedPass.toString())
}) })
/*
it('calls the klicktipp API', () => { it('calls the klicktipp API', () => {
expect(klicktippSignIn).toBeCalledWith( expect(subscribe).toBeCalledWith(
user[0].email, newUser.emailContact.email,
user[0].language, newUser.language,
user[0].firstName, newUser.firstName,
user[0].lastName, newUser.lastName,
) )
}) })
*/
it('returns true', () => { it('returns true', () => {
expect(result).toBeTruthy() expect(result).toBeTruthy()
@ -680,7 +676,6 @@ describe('UserResolver', () => {
expect.objectContaining({ expect.objectContaining({
data: { data: {
login: { login: {
email: 'bibi@bloxberg.de',
firstName: 'Bibi', firstName: 'Bibi',
hasElopage: false, hasElopage: false,
id: expect.any(Number), id: expect.any(Number),
@ -953,7 +948,6 @@ describe('UserResolver', () => {
expect.objectContaining({ expect.objectContaining({
data: { data: {
verifyLogin: { verifyLogin: {
email: 'bibi@bloxberg.de',
firstName: 'Bibi', firstName: 'Bibi',
lastName: 'Bloxberg', lastName: 'Bloxberg',
language: 'de', language: 'de',
@ -1205,6 +1199,28 @@ describe('UserResolver', () => {
}) })
}) })
describe('alias', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('valid alias', () => {
it('updates the user in DB', async () => {
await mutate({
mutation: updateUserInfos,
variables: {
alias: 'bibi_Bloxberg',
},
})
await expect(User.findOne()).resolves.toEqual(
expect.objectContaining({
alias: 'bibi_Bloxberg',
}),
)
})
})
})
describe('language is not valid', () => { describe('language is not valid', () => {
it('throws an error', async () => { it('throws an error', async () => {
jest.clearAllMocks() jest.clearAllMocks()
@ -1310,7 +1326,7 @@ describe('UserResolver', () => {
expect.objectContaining({ expect.objectContaining({
data: { data: {
login: expect.objectContaining({ login: expect.objectContaining({
email: 'bibi@bloxberg.de', firstName: 'Benjamin',
}), }),
}, },
}), }),
@ -1457,7 +1473,6 @@ describe('UserResolver', () => {
expect.objectContaining({ expect.objectContaining({
data: { data: {
login: { login: {
email: 'bibi@bloxberg.de',
firstName: 'Bibi', firstName: 'Bibi',
hasElopage: false, hasElopage: false,
id: expect.any(Number), id: expect.any(Number),
@ -2343,15 +2358,21 @@ describe('UserResolver', () => {
mutation: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
await mutate({
mutation: updateUserInfos,
variables: {
alias: 'bibi',
},
})
}) })
describe('identifier is no gradido ID and no email', () => { describe('identifier is no gradido ID, no email and no alias', () => {
it('throws and logs "Unknown identifier type" error', async () => { it('throws and logs "Unknown identifier type" error', async () => {
await expect( await expect(
query({ query({
query: userQuery, query: userQuery,
variables: { variables: {
identifier: 'identifier', identifier: 'identifier_is_no_valid_alias!',
}, },
}), }),
).resolves.toEqual( ).resolves.toEqual(
@ -2359,7 +2380,10 @@ describe('UserResolver', () => {
errors: [new GraphQLError('Unknown identifier type')], errors: [new GraphQLError('Unknown identifier type')],
}), }),
) )
expect(logger.error).toBeCalledWith('Unknown identifier type', 'identifier') expect(logger.error).toBeCalledWith(
'Unknown identifier type',
'identifier_is_no_valid_alias!',
)
}) })
}) })
@ -2426,6 +2450,57 @@ describe('UserResolver', () => {
) )
}) })
}) })
describe('identifier is found via alias', () => {
it('returns user', async () => {
await expect(
query({
query: userQuery,
variables: {
identifier: 'bibi',
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
user: {
firstName: 'Bibi',
lastName: 'Bloxberg',
},
},
errors: undefined,
}),
)
})
})
})
})
describe('check username', () => {
describe('reserved alias', () => {
it('returns false', async () => {
await expect(
query({ query: checkUsername, variables: { username: 'root' } }),
).resolves.toMatchObject({
data: {
checkUsername: false,
},
errors: undefined,
})
})
})
describe('valid alias', () => {
it('returns true', async () => {
await expect(
query({ query: checkUsername, variables: { username: 'valid' } }),
).resolves.toMatchObject({
data: {
checkUsername: true,
},
errors: undefined,
})
})
}) })
}) })
}) })

View File

@ -35,7 +35,7 @@ import { User } from '@model/User'
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin' import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
import { UserRepository } from '@repository/User' import { UserRepository } from '@repository/User'
import { klicktippSignIn } from '@/apis/KlicktippController' import { subscribe } from '@/apis/KlicktippController'
import { encode } from '@/auth/JWT' import { encode } from '@/auth/JWT'
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
import { CONFIG } from '@/config' import { CONFIG } from '@/config'
@ -73,6 +73,7 @@ import { getTimeDurationObject, printTimeDuration } from '@/util/time'
import { FULL_CREATION_AVAILABLE } from './const/const' import { FULL_CREATION_AVAILABLE } from './const/const'
import { getUserCreations } from './util/creations' import { getUserCreations } from './util/creations'
import { findUserByIdentifier } from './util/findUserByIdentifier' import { findUserByIdentifier } from './util/findUserByIdentifier'
import { validateAlias } from './util/validateAlias'
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs // eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs
const random = require('random-bigint') const random = require('random-bigint')
@ -94,7 +95,7 @@ const newEmailContact = (email: string, userId: number): DbUserContact => {
emailContact.emailChecked = false emailContact.emailChecked = false
emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER
emailContact.emailVerificationCode = random(64) emailContact.emailVerificationCode = random(64)
logger.debug(`newEmailContact...successful: ${emailContact}`) logger.debug('newEmailContact...successful', emailContact)
return emailContact return emailContact
} }
@ -130,7 +131,7 @@ export class UserResolver {
// Elopage Status & Stored PublisherId // Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage(context) user.hasElopage = await this.hasElopage(context)
logger.debug(`verifyLogin... successful: ${user.firstName}.${user.lastName}, ${user.email}`) logger.debug(`verifyLogin... successful: ${user.firstName}.${user.lastName}`)
return user return user
} }
@ -185,7 +186,7 @@ export class UserResolver {
context.setHeaders.push({ context.setHeaders.push({
key: 'token', key: 'token',
value: encode(dbUser.gradidoID), value: await encode(dbUser.gradidoID),
}) })
await EVENT_USER_LOGIN(dbUser) await EVENT_USER_LOGIN(dbUser)
@ -225,7 +226,7 @@ export class UserResolver {
email = email.trim().toLowerCase() email = email.trim().toLowerCase()
if (await checkEmailExists(email)) { if (await checkEmailExists(email)) {
const foundUser = await findUserByEmail(email) const foundUser = await findUserByEmail(email)
logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`) logger.info('DbUser.findOne', email, foundUser)
if (foundUser) { if (foundUser) {
// ATTENTION: this logger-message will be exactly expected during tests, next line // ATTENTION: this logger-message will be exactly expected during tests, next line
@ -238,7 +239,6 @@ export class UserResolver {
const user = new User(communityDbUser) 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.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.gradidoID = uuidv4()
user.email = email
user.firstName = firstName user.firstName = firstName
user.lastName = lastName user.lastName = lastName
user.language = language user.language = language
@ -276,7 +276,7 @@ export class UserResolver {
dbUser.firstName = firstName dbUser.firstName = firstName
dbUser.lastName = lastName dbUser.lastName = lastName
dbUser.language = language dbUser.language = language
dbUser.publisherId = publisherId || 0 dbUser.publisherId = publisherId ?? 0
dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD
logger.debug('new dbUser', dbUser) logger.debug('new dbUser', dbUser)
if (redeemCode) { if (redeemCode) {
@ -383,7 +383,7 @@ export class UserResolver {
throw new LogError('Unable to save email verification code', user.emailContact) throw new LogError('Unable to save email verification code', user.emailContact)
}) })
logger.info(`optInCode for ${email}=${user.emailContact}`) logger.info('optInCode for', email, user.emailContact)
void sendResetPasswordEmail({ void sendResetPasswordEmail({
firstName: user.firstName, firstName: user.firstName,
@ -469,9 +469,9 @@ export class UserResolver {
// TODO do we always signUp the user? How to handle things with old users? // TODO do we always signUp the user? How to handle things with old users?
if (userContact.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) { if (userContact.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) {
try { try {
await klicktippSignIn(userContact.email, user.language, user.firstName, user.lastName) await subscribe(userContact.email, user.language, user.firstName, user.lastName)
logger.debug( logger.debug(
`klicktippSignIn(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`, `subscribe(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
) )
} catch (e) { } catch (e) {
logger.error('Error subscribing to klicktipp', e) logger.error('Error subscribing to klicktipp', e)
@ -487,7 +487,7 @@ export class UserResolver {
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> { async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
logger.info(`queryOptIn(${optIn})...`) logger.info(`queryOptIn(${optIn})...`)
const userContact = await DbUserContact.findOneOrFail({ emailVerificationCode: optIn }) const userContact = await DbUserContact.findOneOrFail({ emailVerificationCode: optIn })
logger.debug(`found optInCode=${userContact}`) logger.debug('found optInCode', userContact)
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) { if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) {
throw new LogError( throw new LogError(
@ -498,6 +498,17 @@ export class UserResolver {
return true return true
} }
@Authorized([RIGHTS.CHECK_USERNAME])
@Query(() => Boolean)
async checkUsername(@Arg('username') username: string): Promise<boolean> {
try {
await validateAlias(username)
return true
} catch {
return false
}
}
@Authorized([RIGHTS.UPDATE_USER_INFOS]) @Authorized([RIGHTS.UPDATE_USER_INFOS])
@Mutation(() => Boolean) @Mutation(() => Boolean)
async updateUserInfos( async updateUserInfos(
@ -505,6 +516,7 @@ export class UserResolver {
{ {
firstName, firstName,
lastName, lastName,
alias,
language, language,
password, password,
passwordNew, passwordNew,
@ -524,6 +536,10 @@ export class UserResolver {
user.lastName = lastName user.lastName = lastName
} }
if (alias && (await validateAlias(alias))) {
user.alias = alias
}
if (language) { if (language) {
if (!isLanguage(language)) { if (!isLanguage(language)) {
throw new LogError('Given language is not a valid language', language) throw new LogError('Given language is not a valid language', language)
@ -587,7 +603,7 @@ export class UserResolver {
logger.info(`hasElopage()...`) logger.info(`hasElopage()...`)
const userEntity = getUser(context) const userEntity = getUser(context)
const elopageBuys = hasElopageBuys(userEntity.emailContact.email) const elopageBuys = hasElopageBuys(userEntity.emailContact.email)
logger.debug(`has ElopageBuys = ${elopageBuys}`) logger.debug('has ElopageBuys', elopageBuys)
return elopageBuys return elopageBuys
} }
@ -644,7 +660,7 @@ export class UserResolver {
return 'user.' + fieldName return 'user.' + fieldName
}), }),
searchText, searchText,
filters || null, filters ?? null,
currentPage, currentPage,
pageSize, pageSize,
) )
@ -710,14 +726,14 @@ export class UserResolver {
// change isAdmin // change isAdmin
switch (user.isAdmin) { switch (user.isAdmin) {
case null: case null:
if (isAdmin === true) { if (isAdmin) {
user.isAdmin = new Date() user.isAdmin = new Date()
} else { } else {
throw new LogError('User is already an usual user') throw new LogError('User is already an usual user')
} }
break break
default: default:
if (isAdmin === false) { if (!isAdmin) {
user.isAdmin = null user.isAdmin = null
} else { } else {
throw new LogError('User is already admin') throw new LogError('User is already admin')

View File

@ -29,10 +29,12 @@ export const validateContribution = (
throw new LogError('No information for available creations for the given date', creationDate) throw new LogError('No information for available creations for the given date', creationDate)
} }
// eslint-disable-next-line security/detect-object-injection
if (amount.greaterThan(creations[index].toString())) { if (amount.greaterThan(creations[index].toString())) {
throw new LogError( throw new LogError(
'The amount to be created exceeds the amount still available for this month', 'The amount to be created exceeds the amount still available for this month',
amount, amount,
// eslint-disable-next-line security/detect-object-injection
creations[index], creations[index],
) )
} }
@ -151,6 +153,7 @@ export const updateCreations = (
if (index < 0) { if (index < 0) {
throw new LogError('You cannot create GDD for a month older than the last three months') throw new LogError('You cannot create GDD for a month older than the last three months')
} }
// eslint-disable-next-line security/detect-object-injection
creations[index] = creations[index].plus(contribution.amount.toString()) creations[index] = creations[index].plus(contribution.amount.toString())
return creations return creations
} }
@ -169,6 +172,7 @@ export const getOpenCreations = async (
return { return {
month: date.getMonth(), month: date.getMonth(),
year: date.getFullYear(), year: date.getFullYear(),
// eslint-disable-next-line security/detect-object-injection
amount: creations[index], amount: creations[index],
} }
}) })

View File

@ -0,0 +1,17 @@
import { Event as DbEvent } from '@entity/Event'
import { User } from '@entity/User'
import { UserContact } from '@entity/UserContact'
export const lastDateTimeEvents = async (
eventType: string,
): Promise<{ email: string; value: Date }[]> => {
return DbEvent.createQueryBuilder('event')
.select('MAX(event.created_at)', 'value')
.leftJoin(User, 'user', 'affected_user_id = user.id')
.leftJoin(UserContact, 'usercontact', 'user.id = usercontact.user_id')
.addSelect('usercontact.email', 'email')
.where('event.type = :eventType', { eventType })
.andWhere('usercontact.email IS NOT NULL')
.groupBy('event.affected_user_id')
.getRawMany()
}

View File

@ -24,7 +24,7 @@ export const findContributions = async (
} }
return DbContribution.findAndCount({ return DbContribution.findAndCount({
where: { where: {
...(statusFilter && statusFilter.length && { contributionStatus: In(statusFilter) }), ...(statusFilter?.length && { contributionStatus: In(statusFilter) }),
...(userId && { userId }), ...(userId && { userId }),
}, },
withDeleted, withDeleted,

View File

@ -4,6 +4,8 @@ import { validate, version } from 'uuid'
import { LogError } from '@/server/LogError' import { LogError } from '@/server/LogError'
import { VALID_ALIAS_REGEX } from './validateAlias'
export const findUserByIdentifier = async (identifier: string): Promise<DbUser> => { export const findUserByIdentifier = async (identifier: string): Promise<DbUser> => {
let user: DbUser | undefined let user: DbUser | undefined
if (validate(identifier) && version(identifier) === 4) { if (validate(identifier) && version(identifier) === 4) {
@ -27,8 +29,12 @@ export const findUserByIdentifier = async (identifier: string): Promise<DbUser>
} }
user = userContact.user user = userContact.user
user.emailContact = userContact user.emailContact = userContact
} else if (VALID_ALIAS_REGEX.exec(identifier)) {
user = await DbUser.findOne({ where: { alias: identifier }, relations: ['emailContact'] })
if (!user) {
throw new LogError('No user found to given identifier', identifier)
}
} else { } else {
// last is alias when implemented
throw new LogError('Unknown identifier type', identifier) throw new LogError('Unknown identifier type', identifier)
} }

View File

@ -14,7 +14,7 @@ export async function transactionLinkList(
filters: TransactionLinkFilters | null, filters: TransactionLinkFilters | null,
user: DbUser, user: DbUser,
): Promise<TransactionLinkResult> { ): Promise<TransactionLinkResult> {
const { withDeleted, withExpired, withRedeemed } = filters || { const { withDeleted, withExpired, withRedeemed } = filters ?? {
withDeleted: false, withDeleted: false,
withExpired: false, withExpired: false,
withRedeemed: false, withRedeemed: false,

View File

@ -0,0 +1,125 @@
import { Connection } from '@dbTools/typeorm'
import { User } from '@entity/User'
import { ApolloServerTestClient } from 'apollo-server-testing'
import { testEnvironment, cleanDB } from '@test/helpers'
import { logger, i18n as localization } from '@test/testSetup'
import { userFactory } from '@/seeds/factory/user'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { validateAlias } from './validateAlias'
let con: Connection
let testEnv: {
mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query']
con: Connection
}
beforeAll(async () => {
testEnv = await testEnvironment(logger, localization)
con = testEnv.con
await cleanDB()
})
afterAll(async () => {
await cleanDB()
await con.close()
})
describe('validate alias', () => {
beforeAll(() => {
jest.clearAllMocks()
})
describe('alias too short', () => {
it('throws and logs an error', async () => {
await expect(validateAlias('Bi')).rejects.toEqual(new Error('Given alias is too short'))
expect(logger.error).toBeCalledWith('Given alias is too short', 'Bi')
})
})
describe('alias too long', () => {
it('throws and logs an error', async () => {
await expect(validateAlias('BibiBloxbergHexHexHex')).rejects.toEqual(
new Error('Given alias is too long'),
)
expect(logger.error).toBeCalledWith('Given alias is too long', 'BibiBloxbergHexHexHex')
})
})
describe('alias contains invalid characters', () => {
it('throws and logs an error', async () => {
await expect(validateAlias('Bibi.Bloxberg')).rejects.toEqual(
new Error('Invalid characters in alias'),
)
expect(logger.error).toBeCalledWith('Invalid characters in alias', 'Bibi.Bloxberg')
})
})
describe('alias is a reserved word', () => {
it('throws and logs an error', async () => {
await expect(validateAlias('admin')).rejects.toEqual(new Error('Alias is not allowed'))
expect(logger.error).toBeCalledWith('Alias is not allowed', 'admin')
})
})
describe('alias is a reserved word with uppercase characters', () => {
it('throws and logs an error', async () => {
await expect(validateAlias('Admin')).rejects.toEqual(new Error('Alias is not allowed'))
expect(logger.error).toBeCalledWith('Alias is not allowed', 'Admin')
})
})
describe('hyphens and underscore', () => {
describe('alias starts with underscore', () => {
it('throws and logs an error', async () => {
await expect(validateAlias('_bibi')).rejects.toEqual(
new Error('Invalid characters in alias'),
)
expect(logger.error).toBeCalledWith('Invalid characters in alias', '_bibi')
})
})
describe('alias contains two following hyphens', () => {
it('throws and logs an error', async () => {
await expect(validateAlias('bi--bi')).rejects.toEqual(
new Error('Invalid characters in alias'),
)
expect(logger.error).toBeCalledWith('Invalid characters in alias', 'bi--bi')
})
})
})
describe('test against existing alias in database', () => {
beforeAll(async () => {
const bibi = await userFactory(testEnv, bibiBloxberg)
const user = await User.findOne({ id: bibi.id })
if (user) {
user.alias = 'b-b'
await user.save()
}
})
describe('alias exists in database', () => {
it('throws and logs an error', async () => {
await expect(validateAlias('b-b')).rejects.toEqual(new Error('Alias already in use'))
expect(logger.error).toBeCalledWith('Alias already in use', 'b-b')
})
})
describe('alias exists in database with in lower-case', () => {
it('throws and logs an error', async () => {
await expect(validateAlias('b-B')).rejects.toEqual(new Error('Alias already in use'))
expect(logger.error).toBeCalledWith('Alias already in use', 'b-B')
})
})
describe('valid alias', () => {
it('resolves to true', async () => {
await expect(validateAlias('bibi')).resolves.toEqual(true)
})
})
})
})

View File

@ -0,0 +1,40 @@
import { Raw } from '@dbTools/typeorm'
import { User as DbUser } from '@entity/User'
import { LogError } from '@/server/LogError'
// eslint-disable-next-line security/detect-unsafe-regex
export const VALID_ALIAS_REGEX = /^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/
const RESERVED_ALIAS = [
'admin',
'email',
'gast',
'gdd',
'gradido',
'guest',
'home',
'root',
'support',
'temp',
'tmp',
'tmp',
'user',
'usr',
'var',
]
export const validateAlias = async (alias: string): Promise<boolean> => {
if (alias.length < 3) throw new LogError('Given alias is too short', alias)
if (alias.length > 20) throw new LogError('Given alias is too long', alias)
if (!alias.match(VALID_ALIAS_REGEX)) throw new LogError('Invalid characters in alias', alias)
if (RESERVED_ALIAS.includes(alias.toLowerCase()))
throw new LogError('Alias is not allowed', alias)
const aliasInUse = await DbUser.find({
where: { alias: Raw((a) => `LOWER(${a}) = "${alias.toLowerCase()}"`) },
})
if (aliasInUse.length !== 0) {
throw new LogError('Alias already in use', alias)
}
return true
}

View File

@ -13,7 +13,7 @@ async function main() {
console.log(`GraphIQL available at http://localhost:${CONFIG.PORT}`) console.log(`GraphIQL available at http://localhost:${CONFIG.PORT}`)
} }
}) })
void startValidateCommunities(Number(CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER)) startValidateCommunities(Number(CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER))
} }
main().catch((e) => { main().catch((e) => {

View File

@ -10,19 +10,6 @@ import { KlickTipp } from '@model/KlickTipp'
import { getKlickTippUser } from '@/apis/KlicktippController' import { getKlickTippUser } from '@/apis/KlicktippController'
import { klickTippLogger as logger } from '@/server/logger' import { klickTippLogger as logger } from '@/server/logger'
// export const klicktippRegistrationMiddleware: MiddlewareFn = async (
// // Only for demo
// /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
// { root, args, context, info },
// next,
// ) => {
// // Do Something here before resolver is called
// const result = await next()
// // Do Something here after resolver is completed
// await klicktippSignIn(result.email, result.language, result.firstName, result.lastName)
// return result
// }
export const klicktippNewsletterStateMiddleware: MiddlewareFn = async ( export const klicktippNewsletterStateMiddleware: MiddlewareFn = async (
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
{ root, args, context, info }, { root, args, context, info },

View File

@ -28,6 +28,7 @@ export const updateUserInfos = gql`
mutation ( mutation (
$firstName: String $firstName: String
$lastName: String $lastName: String
$alias: String
$password: String $password: String
$passwordNew: String $passwordNew: String
$locale: String $locale: String
@ -37,6 +38,7 @@ export const updateUserInfos = gql`
updateUserInfos( updateUserInfos(
firstName: $firstName firstName: $firstName
lastName: $lastName lastName: $lastName
alias: $alias
password: $password password: $password
passwordNew: $passwordNew passwordNew: $passwordNew
language: $locale language: $locale
@ -305,7 +307,6 @@ export const login = gql`
mutation ($email: String!, $password: String!, $publisherId: Int) { mutation ($email: String!, $password: String!, $publisherId: Int) {
login(email: $email, password: $password, publisherId: $publisherId) { login(email: $email, password: $password, publisherId: $publisherId) {
id id
email
firstName firstName
lastName lastName
language language

View File

@ -3,7 +3,6 @@ import { gql } from 'graphql-tag'
export const verifyLogin = gql` export const verifyLogin = gql`
query { query {
verifyLogin { verifyLogin {
email
firstName firstName
lastName lastName
language language
@ -23,32 +22,33 @@ export const queryOptIn = gql`
} }
` `
export const checkUsername = gql`
query ($username: String!) {
checkUsername(username: $username)
}
`
export const transactionsQuery = gql` export const transactionsQuery = gql`
query ( query ($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
$currentPage: Int = 1 transactionList(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
$pageSize: Int = 25 balance {
$order: Order = DESC balance
$onlyCreations: Boolean = false
) {
transactionList(
currentPage: $currentPage
pageSize: $pageSize
order: $order
onlyCreations: $onlyCreations
) {
balanceGDT balanceGDT
count count
balance linkCount
}
transactions { transactions {
id id
typeId typeId
amount amount
balance balance
previousBalance
balanceDate balanceDate
memo memo
linkedUser { linkedUser {
firstName firstName
lastName lastName
gradidoID
} }
decay { decay {
decay decay
@ -56,6 +56,7 @@ export const transactionsQuery = gql`
end end
duration duration
} }
linkId
} }
} }
} }

View File

@ -31,8 +31,8 @@ const context = {
export const cleanDB = async () => { export const cleanDB = async () => {
// this only works as long we do not have foreign key constraints // this only works as long we do not have foreign key constraints
for (let i = 0; i < entities.length; i++) { for (const entity of entities) {
await resetEntity(entities[i]) await resetEntity(entity)
} }
} }
@ -54,9 +54,8 @@ const run = async () => {
logger.info('##seed## clean database successful...') logger.info('##seed## clean database successful...')
// seed the standard users // seed the standard users
for (let i = 0; i < users.length; i++) { for (const user of users) {
const dbUser = await userFactory(seedClient, users[i]) await userFactory(seedClient, user)
logger.info(`##seed## seed standard users[ ${i} ]= ${JSON.stringify(dbUser, null, 2)}`)
} }
logger.info('##seed## seeding all standard users successful...') logger.info('##seed## seeding all standard users successful...')
@ -73,20 +72,20 @@ const run = async () => {
logger.info('##seed## seeding all random users successful...') logger.info('##seed## seeding all random users successful...')
// create GDD // create GDD
for (let i = 0; i < creations.length; i++) { for (const creation of creations) {
await creationFactory(seedClient, creations[i]) await creationFactory(seedClient, creation)
} }
logger.info('##seed## seeding all creations successful...') logger.info('##seed## seeding all creations successful...')
// create Transaction Links // create Transaction Links
for (let i = 0; i < transactionLinks.length; i++) { for (const transactionLink of transactionLinks) {
await transactionLinkFactory(seedClient, transactionLinks[i]) await transactionLinkFactory(seedClient, transactionLink)
} }
logger.info('##seed## seeding all transactionLinks successful...') logger.info('##seed## seeding all transactionLinks successful...')
// create Contribution Links // create Contribution Links
for (let i = 0; i < contributionLinks.length; i++) { for (const contributionLink of contributionLinks) {
await contributionLinkFactory(seedClient, contributionLinks[i]) await contributionLinkFactory(seedClient, contributionLink)
} }
logger.info('##seed## seeding all contributionLinks successful...') logger.info('##seed## seeding all contributionLinks successful...')

View File

@ -1,14 +1,14 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/unbound-method */ /* eslint-disable @typescript-eslint/unbound-method */
import { Connection } from '@dbTools/typeorm' import { Connection as DbConnection } from '@dbTools/typeorm'
import { ApolloServer } from 'apollo-server-express' import { ApolloServer } from 'apollo-server-express'
import express, { Express, json, urlencoded } from 'express' import express, { Express, json, urlencoded } from 'express'
import { Logger } from 'log4js' import { Logger } from 'log4js'
import { CONFIG } from '@/config' import { CONFIG } from '@/config'
import { schema } from '@/graphql/schema' import { schema } from '@/graphql/schema'
import { connection } from '@/typeorm/connection' import { Connection } from '@/typeorm/connection'
import { checkDBVersion } from '@/typeorm/DBVersion' import { checkDBVersion } from '@/typeorm/DBVersion'
import { elopageWebhook } from '@/webhook/elopage' import { elopageWebhook } from '@/webhook/elopage'
@ -21,7 +21,11 @@ import { plugins } from './plugins'
// TODO implement // TODO implement
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity"; // import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
type ServerDef = { apollo: ApolloServer; app: Express; con: Connection } interface ServerDef {
apollo: ApolloServer
app: Express
con: DbConnection
}
export const createServer = async ( export const createServer = async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -33,8 +37,8 @@ export const createServer = async (
logger.debug('createServer...') logger.debug('createServer...')
// open mysql connection // open mysql connection
const con = await connection() const con = await Connection.getInstance()
if (!con || !con.isConnected) { if (!con?.isConnected) {
logger.fatal(`Couldn't open connection to database!`) logger.fatal(`Couldn't open connection to database!`)
throw new Error(`Fatal: Couldn't open connection to database`) throw new Error(`Fatal: Couldn't open connection to database`)
} }

View File

@ -7,6 +7,7 @@ import { configure, getLogger } from 'log4js'
import { CONFIG } from '@/config' import { CONFIG } from '@/config'
// eslint-disable-next-line security/detect-non-literal-fs-filename
const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8')) const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8'))
options.categories.backend.level = CONFIG.LOG_LEVEL options.categories.backend.level = CONFIG.LOG_LEVEL

View File

@ -12,7 +12,7 @@ const setHeadersPlugin = {
return { return {
willSendResponse(requestContext: any) { willSendResponse(requestContext: any) {
const { setHeaders = [] } = requestContext.context const { setHeaders = [] } = requestContext.context
setHeaders.forEach(({ key, value }: { [key: string]: string }) => { setHeaders.forEach(({ key, value }: Record<string, string>) => {
if (requestContext.response.http.headers.get(key)) { if (requestContext.response.http.headers.get(key)) {
requestContext.response.http.headers.set(key, value) requestContext.response.http.headers.set(key, value)
} else { } else {
@ -27,8 +27,8 @@ const setHeadersPlugin = {
const filterVariables = (variables: any) => { const filterVariables = (variables: any) => {
const vars = clonedeep(variables) const vars = clonedeep(variables)
if (vars && vars.password) vars.password = '***' if (vars?.password) vars.password = '***'
if (vars && vars.passwordNew) vars.passwordNew = '***' if (vars?.passwordNew) vars.passwordNew = '***'
return vars return vars
} }

View File

@ -14,10 +14,10 @@ const getDBVersion = async (): Promise<string | null> => {
const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => { const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => {
const dbVersion = await getDBVersion() const dbVersion = await getDBVersion()
if (!dbVersion || dbVersion.indexOf(DB_VERSION) === -1) { if (!dbVersion?.includes(DB_VERSION)) {
logger.error( logger.error(
`Wrong database version detected - the backend requires '${DB_VERSION}' but found '${ `Wrong database version detected - the backend requires '${DB_VERSION}' but found '${
dbVersion || 'None' dbVersion ?? 'None'
}`, }`,
) )
return false return false

View File

@ -1,13 +1,33 @@
// TODO This is super weird - since the entities are defined in another project they have their own globals. // TODO This is super weird - since the entities are defined in another project they have their own globals.
// We cannot use our connection here, but must use the external typeorm installation // We cannot use our connection here, but must use the external typeorm installation
import { Connection, createConnection, FileLogger } from '@dbTools/typeorm' import { Connection as DbConnection, createConnection, FileLogger } from '@dbTools/typeorm'
import { entities } from '@entity/index' import { entities } from '@entity/index'
import { CONFIG } from '@/config' import { CONFIG } from '@/config'
export const connection = async (): Promise<Connection | null> => { // eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class Connection {
private static instance: DbConnection
/**
* The Singleton's constructor should always be private to prevent direct
* construction calls with the `new` operator.
*/
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
private constructor() {}
/**
* The static method that controls the access to the singleton instance.
*
* This implementation let you subclass the Singleton class while keeping
* just one instance of each subclass around.
*/
public static async getInstance(): Promise<DbConnection | null> {
if (Connection.instance) {
return Connection.instance
}
try { try {
return createConnection({ Connection.instance = await createConnection({
name: 'default', name: 'default',
type: 'mysql', type: 'mysql',
host: CONFIG.DB_HOST, host: CONFIG.DB_HOST,
@ -25,9 +45,11 @@ export const connection = async (): Promise<Connection | null> => {
charset: 'utf8mb4_unicode_ci', charset: 'utf8mb4_unicode_ci',
}, },
}) })
return Connection.instance
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error) console.log(error)
return null return null
} }
}
} }

View File

@ -0,0 +1,16 @@
import { Connection } from '@/typeorm/connection'
import { exportEventDataToKlickTipp } from './klicktipp'
async function executeKlicktipp(): Promise<boolean> {
const connection = await Connection.getInstance()
if (connection) {
await exportEventDataToKlickTipp()
await connection.close()
return true
} else {
return false
}
}
void executeKlicktipp()

View File

@ -0,0 +1,65 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Connection } from '@dbTools/typeorm'
import { Event as DbEvent } from '@entity/Event'
import { ApolloServerTestClient } from 'apollo-server-testing'
import { testEnvironment, cleanDB, resetToken } from '@test/helpers'
import { addFieldsToSubscriber } from '@/apis/KlicktippController'
import { creations } from '@/seeds/creation'
import { creationFactory } from '@/seeds/factory/creation'
import { userFactory } from '@/seeds/factory/user'
import { login } from '@/seeds/graphql/mutations'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { peterLustig } from '@/seeds/users/peter-lustig'
import { exportEventDataToKlickTipp } from './klicktipp'
jest.mock('@/apis/KlicktippController')
let mutate: ApolloServerTestClient['mutate'], con: Connection
let testEnv: {
mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query']
con: Connection
}
beforeAll(async () => {
testEnv = await testEnvironment()
mutate = testEnv.mutate
con = testEnv.con
await DbEvent.clear()
})
afterAll(async () => {
await cleanDB()
await con.close()
})
describe('klicktipp', () => {
beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg)
await userFactory(testEnv, peterLustig)
const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de')
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await creationFactory(testEnv, bibisCreation!)
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(() => {
resetToken()
})
describe('exportEventDataToKlickTipp', () => {
it('calls the KlicktippController', async () => {
await exportEventDataToKlickTipp()
expect(addFieldsToSubscriber).toBeCalled()
})
})
})

View File

@ -1,18 +1,14 @@
// eslint-disable @typescript-eslint/no-explicit-any
import { User } from '@entity/User' import { User } from '@entity/User'
import { getKlickTippUser } from '@/apis/KlicktippController' import { getKlickTippUser, addFieldsToSubscriber } from '@/apis/KlicktippController'
import { LogError } from '@/server/LogError' import { EventType } from '@/event/EventType'
import { connection } from '@/typeorm/connection' import { lastDateTimeEvents } from '@/graphql/resolver/util/eventList'
export async function retrieveNotRegisteredEmails(): Promise<string[]> { export async function retrieveNotRegisteredEmails(): Promise<string[]> {
const con = await connection()
if (!con) {
throw new LogError('No connection to database')
}
const users = await User.find({ relations: ['emailContact'] }) const users = await User.find({ relations: ['emailContact'] })
const notRegisteredUser = [] const notRegisteredUser = []
for (let i = 0; i < users.length; i++) { for (const user of users) {
const user = users[i]
try { try {
await getKlickTippUser(user.emailContact.email) await getKlickTippUser(user.emailContact.email)
} catch (err) { } catch (err) {
@ -21,10 +17,39 @@ export async function retrieveNotRegisteredEmails(): Promise<string[]> {
console.log(`${user.emailContact.email}`) console.log(`${user.emailContact.email}`)
} }
} }
await con.close()
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('User die nicht bei KlickTipp vorhanden sind: ', notRegisteredUser) console.log('User die nicht bei KlickTipp vorhanden sind: ', notRegisteredUser)
return notRegisteredUser return notRegisteredUser
} }
void retrieveNotRegisteredEmails() async function klickTippSendFieldToUser(
events: { email: string; value: Date }[],
field: string,
): Promise<void> {
for (const event of events) {
const time = event.value.setSeconds(0)
await addFieldsToSubscriber(event.email, { [field]: Math.trunc(time / 1000) })
}
}
export async function exportEventDataToKlickTipp(): Promise<boolean> {
const lastLoginEvents = await lastDateTimeEvents(EventType.USER_LOGIN)
await klickTippSendFieldToUser(lastLoginEvents, 'field186060')
const registeredEvents = await lastDateTimeEvents(EventType.USER_ACTIVATE_ACCOUNT)
await klickTippSendFieldToUser(registeredEvents, 'field186061')
const receiveTransactionEvents = await lastDateTimeEvents(EventType.TRANSACTION_RECEIVE)
await klickTippSendFieldToUser(receiveTransactionEvents, 'field185674')
const contributionCreateEvents = await lastDateTimeEvents(EventType.TRANSACTION_SEND)
await klickTippSendFieldToUser(contributionCreateEvents, 'field185673')
const linkRedeemedEvents = await lastDateTimeEvents(EventType.TRANSACTION_LINK_REDEEM)
await klickTippSendFieldToUser(linkRedeemedEvents, 'field185676')
const confirmContributionEvents = await lastDateTimeEvents(EventType.ADMIN_CONTRIBUTION_CONFIRM)
await klickTippSendFieldToUser(confirmContributionEvents, 'field185675')
return true
}

View File

@ -1,11 +1,9 @@
import { Decimal } from 'decimal.js-light' import { Decimal } from 'decimal.js-light'
import i18n from 'i18n' import i18n from 'i18n'
export const objectValuesToArray = (obj: { [x: string]: string }): Array<string> => { export const objectValuesToArray = (obj: Record<string, string>): string[] =>
return Object.keys(obj).map(function (key) { // eslint-disable-next-line security/detect-object-injection
return obj[key] Object.keys(obj).map((key) => obj[key])
})
}
export const decimalSeparatorByLanguage = (a: Decimal, language: string): string => { export const decimalSeparatorByLanguage = (a: Decimal, language: string): string => {
const rememberLocaleToRestore = i18n.getLocale() const rememberLocaleToRestore = i18n.getLocale()
@ -14,3 +12,6 @@ export const decimalSeparatorByLanguage = (a: Decimal, language: string): string
i18n.setLocale(rememberLocaleToRestore) i18n.setLocale(rememberLocaleToRestore)
return result return result
} }
export const fullName = (firstName: string, lastName: string): string =>
[firstName, lastName].filter(Boolean).join(' ')

View File

@ -54,6 +54,10 @@ const virtualLinkTransaction = (
creationDate: null, creationDate: null,
contribution: null, contribution: null,
...defaultModelFunctions, ...defaultModelFunctions,
userGradidoID: '',
userName: null,
linkedUserGradidoID: null,
linkedUserName: null,
} }
return new Transaction(linkDbTransaction, user) return new Transaction(linkDbTransaction, user)
} }
@ -84,6 +88,10 @@ const virtualDecayTransaction = (
creationDate: null, creationDate: null,
contribution: null, contribution: null,
...defaultModelFunctions, ...defaultModelFunctions,
userGradidoID: '',
userName: null,
linkedUserGradidoID: null,
linkedUserName: null,
} }
return new Transaction(decayDbTransaction, user) return new Transaction(decayDbTransaction, user)
} }

View File

@ -115,6 +115,7 @@ export const elopageWebhook = async (req: any, res: any): Promise<void> => {
) { ) {
const email = loginElopageBuy.payerEmail const email = loginElopageBuy.payerEmail
// eslint-disable-next-line security/detect-unsafe-regex
const VALIDATE_EMAIL = /^[a-zA-Z0-9.!#$%&?*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/ const VALIDATE_EMAIL = /^[a-zA-Z0-9.!#$%&?*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
const VALIDATE_NAME = /^<>&;]{2,}$/ const VALIDATE_NAME = /^<>&;]{2,}$/
@ -146,7 +147,7 @@ export const elopageWebhook = async (req: any, res: any): Promise<void> => {
email, email,
firstName, firstName,
lastName, lastName,
publisherId: loginElopageBuy.publisherId || 0, // This seemed to be the default value if not set publisherId: loginElopageBuy.publisherId ?? 0, // This seemed to be the default value if not set
}) })
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View File

@ -22,8 +22,8 @@ const context = {
export const cleanDB = async () => { export const cleanDB = async () => {
// this only works as lond we do not have foreign key constraints // this only works as lond we do not have foreign key constraints
for (let i = 0; i < entities.length; i++) { for (const entity of entities) {
await resetEntity(entities[i]) await resetEntity(entity)
} }
} }

View File

@ -382,6 +382,14 @@
dependencies: dependencies:
"@cspotcode/source-map-consumer" "0.8.0" "@cspotcode/source-map-consumer" "0.8.0"
"@eslint-community/eslint-plugin-eslint-comments@^3.2.1":
version "3.2.1"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.1.tgz#3c65061e27f155eae3744c3b30c5a8253a959040"
integrity sha512-/HZbjIGaVO2zLlWX3gRgiHmKRVvvqrC0zVu3eXnIj1ORxoyfGSj50l0PfDfqihyZAqrDYzSMdJesXzFjvAoiLQ==
dependencies:
escape-string-regexp "^1.0.5"
ignore "^5.2.4"
"@eslint-community/eslint-utils@^4.2.0": "@eslint-community/eslint-utils@^4.2.0":
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz#a831e6e468b4b2b5ae42bf658bea015bf10bc518" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz#a831e6e468b4b2b5ae42bf658bea015bf10bc518"
@ -1051,13 +1059,6 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
"@types/jsonwebtoken@^8.5.2":
version "8.5.5"
resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.5.tgz#da5f2f4baee88f052ef3e4db4c1a0afb46cff22c"
integrity sha512-OGqtHQ7N5/Ap/TUwO6IgHDuLiAoTmHhGpNvgkCm/F4N6pKzx/RBSfr2OXZSwC6vkfnsEdb6+7DNZVtiXiwdwFw==
dependencies:
"@types/node" "*"
"@types/keygrip@*": "@types/keygrip@*":
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72"
@ -1994,11 +1995,6 @@ bser@2.1.1:
dependencies: dependencies:
node-int64 "^0.4.0" node-int64 "^0.4.0"
buffer-equal-constant-time@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
buffer-from@^1.0.0: buffer-from@^1.0.0:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
@ -2691,13 +2687,6 @@ duplexer3@^0.1.4:
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
ecdsa-sig-formatter@1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
dependencies:
safe-buffer "^5.0.1"
ee-first@1.1.1: ee-first@1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@ -3005,6 +2994,13 @@ eslint-plugin-promise@^6.1.1:
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz#269a3e2772f62875661220631bd4dafcb4083816" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz#269a3e2772f62875661220631bd4dafcb4083816"
integrity sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig== integrity sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==
eslint-plugin-security@^1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-security/-/eslint-plugin-security-1.7.1.tgz#0e9c4a471f6e4d3ca16413c7a4a51f3966ba16e4"
integrity sha512-sMStceig8AFglhhT2LqlU5r+/fn9OwsA72O5bBuQVTssPCdQAOQzL+oMn/ZcpeUY6KcNfLJArgcrsSULNjYYdQ==
dependencies:
safe-regex "^2.1.1"
eslint-plugin-type-graphql@^1.0.0: eslint-plugin-type-graphql@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-type-graphql/-/eslint-plugin-type-graphql-1.0.0.tgz#d348560ed628d6ca1dfcea35a02891432daafe6b" resolved "https://registry.yarnpkg.com/eslint-plugin-type-graphql/-/eslint-plugin-type-graphql-1.0.0.tgz#d348560ed628d6ca1dfcea35a02891432daafe6b"
@ -3649,7 +3645,7 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0:
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
"gradido-database@file:../database": "gradido-database@file:../database":
version "1.19.1" version "1.20.0"
dependencies: dependencies:
"@types/uuid" "^8.3.4" "@types/uuid" "^8.3.4"
cross-env "^7.0.3" cross-env "^7.0.3"
@ -3977,7 +3973,7 @@ ignore@^5.1.1:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
ignore@^5.2.0: ignore@^5.2.0, ignore@^5.2.4:
version "5.2.4" version "5.2.4"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
@ -4790,6 +4786,11 @@ jest@^27.2.4:
import-local "^3.0.2" import-local "^3.0.2"
jest-cli "^27.2.5" jest-cli "^27.2.5"
jose@^4.14.4:
version "4.14.4"
resolved "https://registry.yarnpkg.com/jose/-/jose-4.14.4.tgz#59e09204e2670c3164ee24cbfe7115c6f8bff9ca"
integrity sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==
js-sdsl@^4.1.4: js-sdsl@^4.1.4:
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711" resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711"
@ -4903,22 +4904,6 @@ jsonfile@^6.0.1:
optionalDependencies: optionalDependencies:
graceful-fs "^4.1.6" graceful-fs "^4.1.6"
jsonwebtoken@^8.5.1:
version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
dependencies:
jws "^3.2.2"
lodash.includes "^4.3.0"
lodash.isboolean "^3.0.3"
lodash.isinteger "^4.0.4"
lodash.isnumber "^3.0.3"
lodash.isplainobject "^4.0.6"
lodash.isstring "^4.0.1"
lodash.once "^4.0.0"
ms "^2.1.1"
semver "^5.6.0"
jstransformer@1.0.0: jstransformer@1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3" resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3"
@ -4938,23 +4923,6 @@ juice@^8.0.0:
slick "^1.12.2" slick "^1.12.2"
web-resource-inliner "^6.0.1" web-resource-inliner "^6.0.1"
jwa@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
dependencies:
buffer-equal-constant-time "1.0.1"
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jws@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
dependencies:
jwa "^1.4.1"
safe-buffer "^5.0.1"
keyv@^3.0.0: keyv@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
@ -5058,46 +5026,11 @@ lodash.get@^4.4.2:
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=
lodash.isboolean@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=
lodash.isinteger@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=
lodash.isnumber@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
lodash.isstring@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
lodash.merge@^4.6.2: lodash.merge@^4.6.2:
version "4.6.2" version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash.once@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
lodash.sortby@^4.7.0: lodash.sortby@^4.7.0:
version "4.7.0" version "4.7.0"
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
@ -6140,6 +6073,11 @@ reflect-metadata@^0.1.13:
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==
regexp-tree@~0.1.1:
version "0.1.27"
resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd"
integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==
regexp.prototype.flags@^1.4.3: regexp.prototype.flags@^1.4.3:
version "1.4.3" version "1.4.3"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"
@ -6279,6 +6217,13 @@ safe-regex-test@^1.0.0:
get-intrinsic "^1.1.3" get-intrinsic "^1.1.3"
is-regex "^1.1.4" is-regex "^1.1.4"
safe-regex@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-2.1.1.tgz#f7128f00d056e2fe5c11e81a1324dd974aadced2"
integrity sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==
dependencies:
regexp-tree "~0.1.1"
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0":
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
@ -6317,7 +6262,7 @@ semver@7.x, semver@^7.3.2, semver@^7.3.4:
dependencies: dependencies:
lru-cache "^6.0.0" lru-cache "^6.0.0"
semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: semver@^5.5.0, semver@^5.7.1:
version "5.7.1" version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==

View File

@ -0,0 +1,139 @@
import Decimal from 'decimal.js-light'
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { Contribution } from '../Contribution'
@Entity('transactions')
export class Transaction extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ type: 'int', unsigned: true, unique: true, nullable: true, default: null })
previous: number | null
@Column({ name: 'type_id', unsigned: true, nullable: false })
typeId: number
@Column({
name: 'transaction_link_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
transactionLinkId?: number | null
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
amount: Decimal
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
balance: Decimal
@Column({
name: 'balance_date',
type: 'datetime',
default: () => 'CURRENT_TIMESTAMP',
nullable: false,
})
balanceDate: Date
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: false,
transformer: DecimalTransformer,
})
decay: Decimal
@Column({
name: 'decay_start',
type: 'datetime',
nullable: true,
default: null,
})
decayStart: Date | null
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
memo: string
@Column({ name: 'creation_date', type: 'datetime', nullable: true, default: null })
creationDate: Date | null
@Column({ name: 'user_id', unsigned: true, nullable: false })
userId: number
@Column({
name: 'user_gradido_id',
type: 'varchar',
length: 36,
nullable: false,
collation: 'utf8mb4_unicode_ci',
})
userGradidoID: string
@Column({
name: 'user_name',
type: 'varchar',
length: 512,
nullable: true,
collation: 'utf8mb4_unicode_ci',
})
userName: string | null
@Column({
name: 'linked_user_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
linkedUserId?: number | null
@Column({
name: 'linked_user_gradido_id',
type: 'varchar',
length: 36,
nullable: true,
collation: 'utf8mb4_unicode_ci',
})
linkedUserGradidoID: string | null
@Column({
name: 'linked_user_name',
type: 'varchar',
length: 512,
nullable: true,
collation: 'utf8mb4_unicode_ci',
})
linkedUserName: string | null
@Column({
name: 'linked_transaction_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
linkedTransactionId?: number | null
@OneToOne(() => Contribution, (contribution) => contribution.transaction)
@JoinColumn({ name: 'id', referencedColumnName: 'transactionId' })
contribution?: Contribution | null
@OneToOne(() => Transaction)
@JoinColumn({ name: 'previous' })
previousTransaction?: Transaction | null
}

View File

@ -1 +1 @@
export { Transaction } from './0036-unique_previous_in_transactions/Transaction' export { Transaction } from './0066-x-community-sendcoins-transactions_table/Transaction'

View File

@ -0,0 +1,74 @@
/* MIGRATION TO add users that have a transaction but do not exist */
/* 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<Array<any>>) {
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `previous` int(10) unsigned DEFAULT NULL NULL AFTER `id`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `type_id` int(10) DEFAULT NULL NULL AFTER `previous`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `transaction_link_id` int(10) unsigned DEFAULT NULL NULL AFTER `type_id`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `amount` decimal(40,20) DEFAULT NULL NULL AFTER `transaction_link_id`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `balance` decimal(40,20) DEFAULT NULL NULL AFTER `amount`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `balance_date` datetime(3) DEFAULT current_timestamp(3) NOT NULL AFTER `balance`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `decay` decimal(40,20) DEFAULT NULL NULL AFTER `balance_date`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `decay_start` datetime(3) DEFAULT NULL NULL AFTER `decay`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL AFTER `decay_start`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `creation_date` datetime(3) DEFAULT NULL NULL AFTER `memo`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `user_id` int(10) unsigned NOT NULL AFTER `creation_date`;',
)
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `user_gradido_id` char(36) DEFAULT NULL NULL AFTER `user_id`;',
)
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `user_name` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL NULL AFTER `user_gradido_id`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `linked_user_id` int(10) unsigned DEFAULT NULL NULL AFTER `user_name`;',
)
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `linked_user_gradido_id` char(36) DEFAULT NULL NULL AFTER `linked_user_id`;',
)
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `linked_user_name` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL NULL AFTER `linked_user_gradido_id`;',
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `linked_transaction_id` int(10) DEFAULT NULL NULL AFTER `linked_user_name`;',
)
await queryFn(
`UPDATE transactions t, users u SET t.user_gradido_id = u.gradido_id, t.user_name = concat(u.first_name, ' ', u.last_name) WHERE t.user_id = u.id and t.user_gradido_id is null;`,
)
await queryFn(
'ALTER TABLE `transactions` MODIFY COLUMN `user_gradido_id` char(36) NOT NULL AFTER `user_id`;',
)
await queryFn(
`UPDATE transactions t, users u SET t.linked_user_gradido_id = u.gradido_id, t.linked_user_name = concat(u.first_name, ' ', u.last_name) WHERE t.linked_user_id = u.id and t.linked_user_gradido_id is null;`,
)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn('ALTER TABLE `transactions` DROP COLUMN `user_gradido_id`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `user_name`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `linked_user_gradido_id`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `linked_user_name`;')
}

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido-database", "name": "gradido-database",
"version": "1.20.0", "version": "1.21.0",
"description": "Gradido Database Tool to execute database migrations", "description": "Gradido Database Tool to execute database migrations",
"main": "src/index.ts", "main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/database", "repository": "https://github.com/gradido/gradido/database",

View File

@ -57,7 +57,7 @@ EMAIL_CODE_REQUEST_TIME=10
WEBHOOK_ELOPAGE_SECRET=secret WEBHOOK_ELOPAGE_SECRET=secret
# Federation # Federation
FEDERATION_DHT_CONFIG_VERSION=v2.2023-02-07 FEDERATION_DHT_CONFIG_VERSION=v3.2023-04-26
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen # if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
# on an hash created from this topic # on an hash created from this topic
# FEDERATION_DHT_TOPIC=GRADIDO_HUB # FEDERATION_DHT_TOPIC=GRADIDO_HUB

View File

@ -117,7 +117,7 @@ server {
# TODO this could be a performance optimization # TODO this could be a performance optimization
#location /vue { #location /vue {
# alias /var/www/html/gradido/frontend/dist; # alias /var/www/html/gradido/frontend/build;
# index index.html; # index index.html;
# #
# location ~* \.(png)$ { # location ~* \.(png)$ {

View File

@ -103,7 +103,7 @@ server {
# TODO this could be a performance optimization # TODO this could be a performance optimization
#location /vue { #location /vue {
# alias /var/www/html/gradido/frontend/dist; # alias /var/www/html/gradido/frontend/build;
# index index.html; # index index.html;
# #
# location ~* \.(png)$ { # location ~* \.(png)$ {

View File

@ -15,6 +15,6 @@ export NVM_DIR="/root/.nvm"
$NPM_BIN install $NPM_BIN install
$NPM_BIN run build $NPM_BIN run build
# prezip for faster deliver throw nginx # prezip for faster deliver throw nginx
cd dist cd build
find . -type f -name "*.css" -exec gzip -9 -k {} \; find . -type f -name "*.css" -exec gzip -9 -k {} \;
find . -type f -name "*.js" -exec gzip -9 -k {} \; find . -type f -name "*.js" -exec gzip -9 -k {} \;

View File

@ -130,6 +130,15 @@ rm -Rf $PROJECT_ROOT/admin/node_modules
rm -Rf $PROJECT_ROOT/dht-node/node_modules rm -Rf $PROJECT_ROOT/dht-node/node_modules
rm -Rf $PROJECT_ROOT/federation/node_modules rm -Rf $PROJECT_ROOT/federation/node_modules
# Remove build folders
# we had problems with corrupted incremtal builds
rm -Rf $PROJECT_ROOT/database/build
rm -Rf $PROJECT_ROOT/backend/build
rm -Rf $PROJECT_ROOT/frontend/build
rm -Rf $PROJECT_ROOT/admin/build
rm -Rf $PROJECT_ROOT/dht-node/build
rm -Rf $PROJECT_ROOT/federation/build
# Regenerate .env files # Regenerate .env files
cp -f $PROJECT_ROOT/database/.env $PROJECT_ROOT/database/.env.bak cp -f $PROJECT_ROOT/database/.env $PROJECT_ROOT/database/.env.bak
cp -f $PROJECT_ROOT/backend/.env $PROJECT_ROOT/backend/.env.bak cp -f $PROJECT_ROOT/backend/.env $PROJECT_ROOT/backend/.env.bak

View File

@ -8,6 +8,10 @@ DB_PASSWORD=$DB_PASSWORD
DB_DATABASE=gradido_community DB_DATABASE=gradido_community
TYPEORM_LOGGING_RELATIVE_PATH=$TYPEORM_LOGGING_RELATIVE_PATH TYPEORM_LOGGING_RELATIVE_PATH=$TYPEORM_LOGGING_RELATIVE_PATH
# Community
COMMUNITY_NAME=$COMMUNITY_NAME
COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION
# Federation # Federation
FEDERATION_DHT_CONFIG_VERSION=$FEDERATION_DHT_CONFIG_VERSION FEDERATION_DHT_CONFIG_VERSION=$FEDERATION_DHT_CONFIG_VERSION
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen # if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen

View File

@ -6,7 +6,7 @@ module.exports = {
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
coverageThreshold: { coverageThreshold: {
global: { global: {
lines: 80, lines: 83,
}, },
}, },
setupFiles: ['<rootDir>/test/testSetup.ts'], setupFiles: ['<rootDir>/test/testSetup.ts'],

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido-dht-node", "name": "gradido-dht-node",
"version": "1.20.0", "version": "1.21.0",
"description": "Gradido dht-node module", "description": "Gradido dht-node module",
"main": "src/index.ts", "main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/", "repository": "https://github.com/gradido/gradido/",
@ -23,7 +23,8 @@
"nodemon": "^2.0.20", "nodemon": "^2.0.20",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.2", "tsconfig-paths": "^4.1.2",
"typescript": "^4.9.4" "typescript": "^4.9.4",
"uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@types/dotenv": "^8.2.0", "@types/dotenv": "^8.2.0",
@ -31,6 +32,7 @@
"@types/node": "^18.11.18", "@types/node": "^18.11.18",
"@typescript-eslint/eslint-plugin": "^5.48.0", "@typescript-eslint/eslint-plugin": "^5.48.0",
"@typescript-eslint/parser": "^5.48.0", "@typescript-eslint/parser": "^5.48.0",
"@types/uuid": "^8.3.4",
"eslint": "^8.31.0", "eslint": "^8.31.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^17.0.0", "eslint-config-standard": "^17.0.0",

View File

@ -3,13 +3,13 @@ import dotenv from 'dotenv'
dotenv.config() dotenv.config()
const constants = { const constants = {
DB_VERSION: '0065-refactor_communities_table', DB_VERSION: '0066-x-community-sendcoins-transactions_table',
LOG4JS_CONFIG: 'log4js-config.json', LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info // default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info', LOG_LEVEL: process.env.LOG_LEVEL || 'info',
CONFIG_VERSION: { CONFIG_VERSION: {
DEFAULT: 'DEFAULT', DEFAULT: 'DEFAULT',
EXPECTED: 'v2.2023-02-07', EXPECTED: 'v3.2023-04-26',
CURRENT: '', CURRENT: '',
}, },
} }
@ -28,6 +28,12 @@ const database = {
process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.dht-node.log', process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.dht-node.log',
} }
const community = {
COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung',
COMMUNITY_DESCRIPTION:
process.env.COMMUNITY_DESCRIPTION || 'Gradido-Community einer lokalen Entwicklungsumgebung.',
}
const federation = { const federation = {
FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || 'GRADIDO_HUB', FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || 'GRADIDO_HUB',
FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null, FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
@ -51,6 +57,7 @@ const CONFIG = {
...constants, ...constants,
...server, ...server,
...database, ...database,
...community,
...federation, ...federation,
} }

View File

@ -5,8 +5,10 @@ import { startDHT } from './index'
import DHT from '@hyperswarm/dht' import DHT from '@hyperswarm/dht'
import CONFIG from '@/config' import CONFIG from '@/config'
import { logger } from '@test/testSetup' import { logger } from '@test/testSetup'
import { Community as DbCommunity } from '@entity/Community'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { testEnvironment, cleanDB } from '@test/helpers' import { testEnvironment, cleanDB } from '@test/helpers'
import { validate as validateUUID, version as versionUUID } from 'uuid'
CONFIG.FEDERATION_DHT_SEED = '64ebcb0e3ad547848fef4197c6e2332f' CONFIG.FEDERATION_DHT_SEED = '64ebcb0e3ad547848fef4197c6e2332f'
@ -114,6 +116,9 @@ describe('federation', () => {
const hashSpy = jest.spyOn(DHT, 'hash') const hashSpy = jest.spyOn(DHT, 'hash')
const keyPairSpy = jest.spyOn(DHT, 'keyPair') const keyPairSpy = jest.spyOn(DHT, 'keyPair')
beforeEach(async () => { beforeEach(async () => {
CONFIG.FEDERATION_COMMUNITY_URL = 'https://test.gradido.net'
CONFIG.COMMUNITY_NAME = 'Gradido Test Community'
CONFIG.COMMUNITY_DESCRIPTION = 'Community to test the federation'
DHT.mockClear() DHT.mockClear()
jest.clearAllMocks() jest.clearAllMocks()
await cleanDB() await cleanDB()
@ -132,6 +137,64 @@ describe('federation', () => {
expect(DHT).toBeCalledWith({ keyPair: keyPairMock }) expect(DHT).toBeCalledWith({ keyPair: keyPairMock })
}) })
it('stores the home community in community table ', async () => {
const result = await DbCommunity.find()
expect(result).toEqual([
expect.objectContaining({
id: expect.any(Number),
foreign: false,
url: 'https://test.gradido.net/api/',
publicKey: expect.any(Buffer),
communityUuid: expect.any(String),
authenticatedAt: null,
name: 'Gradido Test Community',
description: 'Community to test the federation',
creationDate: expect.any(Date),
createdAt: expect.any(Date),
updatedAt: null,
}),
])
expect(validateUUID(result[0].communityUuid ? result[0].communityUuid : '')).toEqual(true)
expect(versionUUID(result[0].communityUuid ? result[0].communityUuid : '')).toEqual(4)
})
it('creates 3 entries in table federated_communities', async () => {
const result = await DbFederatedCommunity.find({ order: { id: 'ASC' } })
await expect(result).toHaveLength(3)
await expect(result).toEqual([
expect.objectContaining({
id: expect.any(Number),
foreign: false,
publicKey: expect.any(Buffer),
apiVersion: '1_0',
endPoint: 'https://test.gradido.net/api/',
lastAnnouncedAt: null,
createdAt: expect.any(Date),
updatedAt: null,
}),
expect.objectContaining({
id: expect.any(Number),
foreign: false,
publicKey: expect.any(Buffer),
apiVersion: '1_1',
endPoint: 'https://test.gradido.net/api/',
lastAnnouncedAt: null,
createdAt: expect.any(Date),
updatedAt: null,
}),
expect.objectContaining({
id: expect.any(Number),
foreign: false,
publicKey: expect.any(Buffer),
apiVersion: '2_0',
endPoint: 'https://test.gradido.net/api/',
lastAnnouncedAt: null,
createdAt: expect.any(Date),
updatedAt: null,
}),
])
})
describe('DHT node', () => { describe('DHT node', () => {
it('creates a server', () => { it('creates a server', () => {
expect(nodeCreateServerMock).toBeCalled() expect(nodeCreateServerMock).toBeCalled()
@ -780,21 +843,21 @@ describe('federation', () => {
socketEventMocks.open() socketEventMocks.open()
}) })
it.skip('calls socket write with own api versions', () => { it('calls socket write with own api versions', () => {
expect(socketWriteMock).toBeCalledWith( expect(socketWriteMock).toBeCalledWith(
Buffer.from( Buffer.from(
JSON.stringify([ JSON.stringify([
{ {
api: '1_0', api: '1_0',
url: 'http://localhost/api/', url: 'https://test.gradido.net/api/',
}, },
{ {
api: '1_1', api: '1_1',
url: 'http://localhost/api/', url: 'https://test.gradido.net/api/',
}, },
{ {
api: '2_0', api: '2_0',
url: 'http://localhost/api/', url: 'https://test.gradido.net/api/',
}, },
]), ]),
), ),
@ -804,5 +867,101 @@ describe('federation', () => {
}) })
}) })
}) })
describe('restart DHT', () => {
let homeCommunity: DbCommunity
let federatedCommunities: DbFederatedCommunity[]
describe('without changes', () => {
beforeEach(async () => {
DHT.mockClear()
jest.clearAllMocks()
homeCommunity = (await DbCommunity.find())[0]
federatedCommunities = await DbFederatedCommunity.find({ order: { id: 'ASC' } })
await startDHT(TEST_TOPIC)
})
it('does not change home community in community table except updated at column ', async () => {
await expect(DbCommunity.find()).resolves.toEqual([
{
...homeCommunity,
updatedAt: expect.any(Date),
},
])
})
it('rewrites the 3 entries in table federated_communities', async () => {
const result = await DbFederatedCommunity.find()
await expect(result).toHaveLength(3)
await expect(result).toEqual([
{
...federatedCommunities[0],
id: expect.any(Number),
createdAt: expect.any(Date),
},
{
...federatedCommunities[1],
id: expect.any(Number),
createdAt: expect.any(Date),
},
{
...federatedCommunities[2],
id: expect.any(Number),
createdAt: expect.any(Date),
},
])
})
})
describe('changeing URL, name and description', () => {
beforeEach(async () => {
CONFIG.FEDERATION_COMMUNITY_URL = 'https://test2.gradido.net'
CONFIG.COMMUNITY_NAME = 'Second Gradido Test Community'
CONFIG.COMMUNITY_DESCRIPTION = 'Another Community to test the federation'
DHT.mockClear()
jest.clearAllMocks()
homeCommunity = (await DbCommunity.find())[0]
federatedCommunities = await DbFederatedCommunity.find({ order: { id: 'ASC' } })
await startDHT(TEST_TOPIC)
})
it('updates URL, name, description and updated at columns ', async () => {
await expect(DbCommunity.find()).resolves.toEqual([
{
...homeCommunity,
url: 'https://test2.gradido.net/api/',
name: 'Second Gradido Test Community',
description: 'Another Community to test the federation',
updatedAt: expect.any(Date),
},
])
})
it('rewrites the 3 entries in table federated_communities with new endpoint', async () => {
const result = await DbFederatedCommunity.find()
await expect(result).toHaveLength(3)
await expect(result).toEqual([
{
...federatedCommunities[0],
id: expect.any(Number),
createdAt: expect.any(Date),
endPoint: 'https://test2.gradido.net/api/',
},
{
...federatedCommunities[1],
id: expect.any(Number),
createdAt: expect.any(Date),
endPoint: 'https://test2.gradido.net/api/',
},
{
...federatedCommunities[2],
id: expect.any(Number),
createdAt: expect.any(Date),
endPoint: 'https://test2.gradido.net/api/',
},
])
})
})
})
}) })
}) })

View File

@ -4,10 +4,15 @@ import DHT from '@hyperswarm/dht'
import { logger } from '@/server/logger' import { logger } from '@/server/logger'
import CONFIG from '@/config' import CONFIG from '@/config'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { Community as DbCommunity } from '@entity/Community'
import { v4 as uuidv4 } from 'uuid'
const KEY_SECRET_SEEDBYTES = 32 const KEY_SECRET_SEEDBYTES = 32
const getSeed = (): Buffer | null => const getSeed = (): Buffer | null => {
CONFIG.FEDERATION_DHT_SEED ? Buffer.alloc(KEY_SECRET_SEEDBYTES, CONFIG.FEDERATION_DHT_SEED) : null return CONFIG.FEDERATION_DHT_SEED
? Buffer.alloc(KEY_SECRET_SEEDBYTES, CONFIG.FEDERATION_DHT_SEED)
: null
}
const POLLTIME = 20000 const POLLTIME = 20000
const SUCCESSTIME = 120000 const SUCCESSTIME = 120000
@ -28,10 +33,12 @@ export const startDHT = async (topic: string): Promise<void> => {
try { try {
const TOPIC = DHT.hash(Buffer.from(topic)) const TOPIC = DHT.hash(Buffer.from(topic))
const keyPair = DHT.keyPair(getSeed()) const keyPair = DHT.keyPair(getSeed())
logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`) const pubKeyString = keyPair.publicKey.toString('hex')
logger.info(`keyPairDHT: publicKey=${pubKeyString}`)
logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`) logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`)
await writeHomeCommunityEntry(pubKeyString)
const ownApiVersions = await writeFederatedHomeCommunityEnries(keyPair.publicKey) const ownApiVersions = await writeFederatedHomeCommunityEntries(pubKeyString)
logger.info(`ApiList: ${JSON.stringify(ownApiVersions)}`) logger.info(`ApiList: ${JSON.stringify(ownApiVersions)}`)
const node = new DHT({ keyPair }) const node = new DHT({ keyPair })
@ -138,7 +145,7 @@ export const startDHT = async (topic: string): Promise<void> => {
data.peers.forEach((peer: any) => { data.peers.forEach((peer: any) => {
const pubKey = peer.publicKey.toString('hex') const pubKey = peer.publicKey.toString('hex')
if ( if (
pubKey !== keyPair.publicKey.toString('hex') && pubKey !== pubKeyString &&
!successfulRequests.includes(pubKey) && !successfulRequests.includes(pubKey) &&
!errorfulRequests.includes(pubKey) && !errorfulRequests.includes(pubKey) &&
!collectedPubKeys.includes(pubKey) !collectedPubKeys.includes(pubKey)
@ -179,7 +186,7 @@ export const startDHT = async (topic: string): Promise<void> => {
} }
} }
async function writeFederatedHomeCommunityEnries(pubKey: any): Promise<CommunityApi[]> { async function writeFederatedHomeCommunityEntries(pubKey: string): Promise<CommunityApi[]> {
const homeApiVersions: CommunityApi[] = Object.values(ApiVersionType).map(function (apiEnum) { const homeApiVersions: CommunityApi[] = Object.values(ApiVersionType).map(function (apiEnum) {
const comApi: CommunityApi = { const comApi: CommunityApi = {
api: apiEnum, api: apiEnum,
@ -189,21 +196,65 @@ async function writeFederatedHomeCommunityEnries(pubKey: any): Promise<Community
}) })
try { try {
// first remove privious existing homeCommunity entries // first remove privious existing homeCommunity entries
DbFederatedCommunity.createQueryBuilder().delete().where({ foreign: false }).execute() await DbFederatedCommunity.createQueryBuilder().delete().where({ foreign: false }).execute()
for (const homeApiVersion of homeApiVersions) {
homeApiVersions.forEach(async function (homeApi) { const homeCom = DbFederatedCommunity.create()
const homeCom = new DbFederatedCommunity()
homeCom.foreign = false homeCom.foreign = false
homeCom.apiVersion = homeApi.api homeCom.apiVersion = homeApiVersion.api
homeCom.endPoint = homeApi.url homeCom.endPoint = homeApiVersion.url
homeCom.publicKey = pubKey.toString('hex') homeCom.publicKey = Buffer.from(pubKey)
// this will NOT update the updatedAt column, to distingue between a normal update and the last announcement
await DbFederatedCommunity.insert(homeCom) await DbFederatedCommunity.insert(homeCom)
logger.info(`federation home-community inserted successfully: ${JSON.stringify(homeCom)}`) logger.info(`federation home-community inserted successfully:`, homeApiVersion)
}) }
} catch (err) { } catch (err) {
throw new Error(`Federation: Error writing HomeCommunity-Entries: ${err}`) throw new Error(`Federation: Error writing federated HomeCommunity-Entries: ${err}`)
} }
return homeApiVersions return homeApiVersions
} }
async function writeHomeCommunityEntry(pubKey: string): Promise<void> {
try {
// check for existing homeCommunity entry
let homeCom = await DbCommunity.findOne({
foreign: false,
publicKey: Buffer.from(pubKey),
})
if (!homeCom) {
// check if a homecommunity with a different publicKey still exists
homeCom = await DbCommunity.findOne({ foreign: false })
}
if (homeCom) {
// simply update the existing entry, but it MUST keep the ID and UUID because of possible relations
homeCom.publicKey = Buffer.from(pubKey)
homeCom.url = CONFIG.FEDERATION_COMMUNITY_URL + '/api/'
homeCom.name = CONFIG.COMMUNITY_NAME
homeCom.description = CONFIG.COMMUNITY_DESCRIPTION
await DbCommunity.save(homeCom)
logger.info(`home-community updated successfully:`, homeCom)
} else {
// insert a new homecommunity entry including a new ID and a new but ensured unique UUID
homeCom = new DbCommunity()
homeCom.foreign = false
homeCom.publicKey = Buffer.from(pubKey)
homeCom.communityUuid = await newCommunityUuid()
homeCom.url = CONFIG.FEDERATION_COMMUNITY_URL + '/api/'
homeCom.name = CONFIG.COMMUNITY_NAME
homeCom.description = CONFIG.COMMUNITY_DESCRIPTION
homeCom.creationDate = new Date()
await DbCommunity.insert(homeCom)
logger.info(`home-community inserted successfully:`, homeCom)
}
} catch (err) {
throw new Error(`Federation: Error writing HomeCommunity-Entry: ${err}`)
}
}
const newCommunityUuid = async (): Promise<string> => {
while (true) {
const communityUuid = uuidv4()
if ((await DbCommunity.count({ where: { communityUuid } })) === 0) {
return communityUuid
}
logger.info('CommunityUuid creation conflict...', communityUuid)
}
}

View File

@ -21,9 +21,8 @@ async function main() {
logger.fatal('Fatal: Database Version incorrect') logger.fatal('Fatal: Database Version incorrect')
throw new Error('Fatal: Database Version incorrect') throw new Error('Fatal: Database Version incorrect')
} }
logger.debug(`dhtseed set by CONFIG.FEDERATION_DHT_SEED=${CONFIG.FEDERATION_DHT_SEED}`)
// eslint-disable-next-line no-console logger.info(
console.log(
`starting Federation on ${CONFIG.FEDERATION_DHT_TOPIC} ${ `starting Federation on ${CONFIG.FEDERATION_DHT_TOPIC} ${
CONFIG.FEDERATION_DHT_SEED ? 'with seed...' : 'without seed...' CONFIG.FEDERATION_DHT_SEED ? 'with seed...' : 'without seed...'
}`, }`,

View File

@ -22,8 +22,8 @@ const context = {
export const cleanDB = async () => { export const cleanDB = async () => {
// this only works as long we do not have foreign key constraints // this only works as long we do not have foreign key constraints
for (let i = 0; i < entities.length; i++) { for (const entity of entities) {
await resetEntity(entities[i]) await resetEntity(entity)
} }
} }

View File

@ -769,6 +769,11 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
"@types/uuid@^8.3.4":
version "8.3.4"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
"@types/yargs-parser@*": "@types/yargs-parser@*":
version "21.0.0" version "21.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
@ -4138,6 +4143,11 @@ url-parse@^1.5.3:
querystringify "^2.1.1" querystringify "^2.1.1"
requires-port "^1.0.0" requires-port "^1.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==
v8-compile-cache-lib@^3.0.1: v8-compile-cache-lib@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"

View File

@ -58,7 +58,7 @@ export default defineConfig({
mailserverURL: 'http://localhost:1080', mailserverURL: 'http://localhost:1080',
loginQuery: `mutation ($email: String!, $password: String!, $publisherId: Int) { loginQuery: `mutation ($email: String!, $password: String!, $publisherId: Int) {
login(email: $email, password: $password, publisherId: $publisherId) { login(email: $email, password: $password, publisherId: $publisherId) {
email id
firstName firstName
lastName lastName
language language

View File

@ -35,6 +35,6 @@ Cypress.Commands.add('login', (email, password) => {
} }
cy.visit('/') cy.visit('/')
window.localStorage.setItem('vuex', JSON.stringify(vuexToken)) window.localStorage.setItem('gradido-frontend', JSON.stringify(vuexToken))
}) })
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido-federation", "name": "gradido-federation",
"version": "1.20.0", "version": "1.21.0",
"description": "Gradido federation module providing Gradido-Hub-Federation and versioned API for inter community communication", "description": "Gradido federation module providing Gradido-Hub-Federation and versioned API for inter community communication",
"main": "src/index.ts", "main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/federation", "repository": "https://github.com/gradido/gradido/federation",

View File

@ -11,7 +11,7 @@ Decimal.set({
*/ */
const constants = { const constants = {
DB_VERSION: '0065-refactor_communities_table', DB_VERSION: '0066-x-community-sendcoins-transactions_table',
// DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 // DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json', LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info // default log level on production should be info

View File

@ -23,8 +23,8 @@ const setHeadersPlugin = {
const filterVariables = (variables: any) => { const filterVariables = (variables: any) => {
const vars = clonedeep(variables) const vars = clonedeep(variables)
if (vars.password) vars.password = '***' if (vars && vars.password) vars.password = '***'
if (vars.passwordNew) vars.passwordNew = '***' if (vars && vars.passwordNew) vars.passwordNew = '***'
return vars return vars
} }

View File

@ -1,3 +1,3 @@
node_modules/ node_modules/
dist/ build/
coverage/ coverage/

2
frontend/.gitignore vendored
View File

@ -1,6 +1,6 @@
.DS_Store .DS_Store
node_modules/ node_modules/
dist/ build/
.cache/ .cache/
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*

View File

@ -84,7 +84,7 @@ CMD /bin/sh -c "yarn run dev"
FROM base as production FROM base as production
# Copy "binary"-files from build image # Copy "binary"-files from build image
COPY --from=build ${DOCKER_WORKDIR}/dist ./dist COPY --from=build ${DOCKER_WORKDIR}/build ./build
# We also copy the node_modules express and serve-static for the run script # We also copy the node_modules express and serve-static for the run script
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
# Copy static files # Copy static files

View File

@ -1,13 +1,13 @@
{ {
"name": "bootstrap-vue-gradido-wallet", "name": "bootstrap-vue-gradido-wallet",
"version": "1.20.0", "version": "1.21.0",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "node run/server.js", "start": "node run/server.js",
"serve": "vue-cli-service serve --open", "serve": "vue-cli-service serve --open",
"build": "vue-cli-service build", "build": "vue-cli-service build",
"dev": "yarn run serve", "dev": "yarn run serve",
"analyse-bundle": "yarn build && webpack-bundle-analyzer dist/webpack.stats.json", "analyse-bundle": "yarn build && webpack-bundle-analyzer build/webpack.stats.json",
"lint": "eslint --max-warnings=0 --ext .js,.vue,.json .", "lint": "eslint --max-warnings=0 --ext .js,.vue,.json .",
"stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'", "stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'",
"test": "cross-env TZ=UTC jest", "test": "cross-env TZ=UTC jest",
@ -50,6 +50,7 @@
"prettier": "^2.2.1", "prettier": "^2.2.1",
"qrcanvas-vue": "2.1.1", "qrcanvas-vue": "2.1.1",
"regenerator-runtime": "^0.13.7", "regenerator-runtime": "^0.13.7",
"uuid": "^9.0.0",
"vee-validate": "^3.4.5", "vee-validate": "^3.4.5",
"vue": "2.6.12", "vue": "2.6.12",
"vue-apollo": "^3.0.7", "vue-apollo": "^3.0.7",

View File

@ -9,10 +9,10 @@ const port = process.env.PORT || 3000
// Express Server // Express Server
const app = express() const app = express()
// Serve files // Serve files
app.use(express.static(path.join(__dirname, '../dist'))) app.use(express.static(path.join(__dirname, '../build')))
// Default to index.html // Default to index.html
app.get('*', (req, res) => { app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../dist/index.html')) res.sendFile(path.join(__dirname, '../build/index.html'))
}) })
app.listen(port, hostname, () => { app.listen(port, hostname, () => {

View File

@ -3,15 +3,23 @@
<div class="bg-white appBoxShadow gradido-border-radius p-3"> <div class="bg-white appBoxShadow gradido-border-radius p-3">
<div class="h3 mb-4">{{ $t('form.send_check') }}</div> <div class="h3 mb-4">{{ $t('form.send_check') }}</div>
<b-row class="mt-5"> <b-row class="mt-5">
<b-col cols="2"></b-col> <b-col cols="12">
<b-col> <b-row class="mt-3">
<div class="h4">{{ userName ? userName : identifier }}</div> <b-col class="h5">{{ $t('form.recipientCommunity') }}</b-col>
<div class="mt-3 h5">{{ $t('form.memo') }}</div> <b-col>{{ communityName }}</b-col>
<div>{{ memo }}</div> </b-row>
</b-col> <b-row>
<b-col cols="3"> <b-col class="h5">{{ $t('form.recipient') }}</b-col>
<div class="small">{{ $t('send_gdd') }}</div> <b-col>{{ userName ? userName : identifier }}</b-col>
<div>{{ amount | GDD }}</div> </b-row>
<b-row>
<b-col class="h5">{{ $t('form.amount') }}</b-col>
<b-col>{{ amount | GDD }}</b-col>
</b-row>
<b-row>
<b-col class="h5">{{ $t('form.memo') }}</b-col>
<b-col>{{ memo }}</b-col>
</b-row>
</b-col> </b-col>
</b-row> </b-row>
@ -58,6 +66,8 @@
</div> </div>
</template> </template>
<script> <script>
import { COMMUNITY_NAME } from '@/config'
export default { export default {
name: 'TransactionConfirmationSend', name: 'TransactionConfirmationSend',
props: { props: {
@ -70,6 +80,7 @@ export default {
data() { data() {
return { return {
disabled: false, disabled: false,
communityName: COMMUNITY_NAME,
} }
}, },
} }

View File

@ -71,9 +71,9 @@ describe('TransactionForm', () => {
}) })
describe('with balance <= 0.00 GDD the form is disabled', () => { describe('with balance <= 0.00 GDD the form is disabled', () => {
it('has a disabled input field of type email', () => { it('has a disabled input field of type text', () => {
expect( expect(
wrapper.find('div[data-test="input-email"]').find('input').attributes('disabled'), wrapper.find('div[data-test="input-identifier"]').find('input').attributes('disabled'),
).toBe('disabled') ).toBe('disabled')
}) })
@ -116,51 +116,54 @@ describe('TransactionForm', () => {
expect(wrapper.vm.radioSelected).toBe(SEND_TYPES.send) expect(wrapper.vm.radioSelected).toBe(SEND_TYPES.send)
}) })
describe('email field', () => { describe('identifier field', () => {
it('has an input field of type email', () => { it('has an input field of type text', () => {
expect( expect(
wrapper.find('div[data-test="input-email"]').find('input').attributes('type'), wrapper.find('div[data-test="input-identifier"]').find('input').attributes('type'),
).toBe('email') ).toBe('text')
}) })
it('has a label form.receiver', () => { it('has a label form.recipient', () => {
expect(wrapper.find('div[data-test="input-email"]').find('label').text()).toBe( expect(wrapper.find('div[data-test="input-identifier"]').find('label').text()).toBe(
'form.recipient', 'form.recipient',
) )
}) })
it('has a placeholder "E-Mail"', () => { it('has a placeholder for identifier', () => {
expect( expect(
wrapper.find('div[data-test="input-email"]').find('input').attributes('placeholder'), wrapper
).toBe('form.email') .find('div[data-test="input-identifier"]')
.find('input')
.attributes('placeholder'),
).toBe('form.identifier')
}) })
it('flushes an error message when no valid email is given', async () => { it('flushes an error message when no valid identifier is given', async () => {
await wrapper.find('div[data-test="input-email"]').find('input').setValue('a') await wrapper.find('div[data-test="input-identifier"]').find('input').setValue('a')
await flushPromises() await flushPromises()
expect( expect(
wrapper.find('div[data-test="input-email"]').find('.invalid-feedback').text(), wrapper.find('div[data-test="input-identifier"]').find('.invalid-feedback').text(),
).toBe('validations.messages.email') ).toBe('form.validation.valid-identifier')
}) })
// TODO:SKIPPED there is no check that the email being sent to is the same as the user's email. // TODO:SKIPPED there is no check that the email being sent to is the same as the user's email.
it.skip('flushes an error message when email is the email of logged in user', async () => { it.skip('flushes an error message when email is the email of logged in user', async () => {
await wrapper await wrapper
.find('div[data-test="input-email"]') .find('div[data-test="input-identifier"]')
.find('input') .find('input')
.setValue('user@example.org') .setValue('user@example.org')
await flushPromises() await flushPromises()
expect( expect(
wrapper.find('div[data-test="input-email"]').find('.invalid-feedback').text(), wrapper.find('div[data-test="input-identifier"]').find('.invalid-feedback').text(),
).toBe('form.validation.is-not') ).toBe('form.validation.is-not')
}) })
it('trims the email after blur', async () => { it('trims the identifier after blur', async () => {
await wrapper await wrapper
.find('div[data-test="input-email"]') .find('div[data-test="input-identifier"]')
.find('input') .find('input')
.setValue(' valid@email.com ') .setValue(' valid@email.com ')
await wrapper.find('div[data-test="input-email"]').find('input').trigger('blur') await wrapper.find('div[data-test="input-identifier"]').find('input').trigger('blur')
await flushPromises() await flushPromises()
expect(wrapper.vm.form.identifier).toBe('valid@email.com') expect(wrapper.vm.form.identifier).toBe('valid@email.com')
}) })
@ -304,7 +307,7 @@ Die ganze Welt bezwingen.“`)
it('clears all fields on click', async () => { it('clears all fields on click', async () => {
await wrapper await wrapper
.find('div[data-test="input-email"]') .find('div[data-test="input-identifier"]')
.find('input') .find('input')
.setValue('someone@watches.tv') .setValue('someone@watches.tv')
await wrapper.find('div[data-test="input-amount"]').find('input').setValue('87.23') await wrapper.find('div[data-test="input-amount"]').find('input').setValue('87.23')
@ -327,7 +330,7 @@ Die ganze Welt bezwingen.“`)
describe('submit', () => { describe('submit', () => {
beforeEach(async () => { beforeEach(async () => {
await wrapper await wrapper
.find('div[data-test="input-email"]') .find('div[data-test="input-identifier"]')
.find('input') .find('input')
.setValue('someone@watches.tv') .setValue('someone@watches.tv')
await wrapper.find('div[data-test="input-amount"]').find('input').setValue('87.23') await wrapper.find('div[data-test="input-amount"]').find('input').setValue('87.23')
@ -380,8 +383,8 @@ Die ganze Welt bezwingen.“`)
}) })
describe('query for username with success', () => { describe('query for username with success', () => {
it('has no email input field', () => { it('has no identifier input field', () => {
expect(wrapper.find('div[data-test="input-email"]').exists()).toBe(false) expect(wrapper.find('div[data-test="input-identifier"]').exists()).toBe(false)
}) })
it('queries the username', () => { it('queries the username', () => {

View File

@ -49,12 +49,20 @@
<b-row> <b-row>
<b-col> <b-col>
<b-row> <b-row>
<b-col class="mb-4" cols="12" v-if="radioSelected === sendTypes.send">
<b-row>
<b-col>{{ $t('form.recipientCommunity') }}</b-col>
</b-row>
<b-row>
<b-col class="font-weight-bold">{{ communityName }}</b-col>
</b-row>
</b-col>
<b-col cols="12" v-if="radioSelected === sendTypes.send"> <b-col cols="12" v-if="radioSelected === sendTypes.send">
<div v-if="!gradidoID"> <div v-if="!gradidoID">
<input-email <input-identifier
:name="$t('form.recipient')" :name="$t('form.recipient')"
:label="$t('form.recipient')" :label="$t('form.recipient')"
:placeholder="$t('form.email')" :placeholder="$t('form.identifier')"
v-model="form.identifier" v-model="form.identifier"
:disabled="isBalanceDisabled" :disabled="isBalanceDisabled"
@onValidation="onValidation" @onValidation="onValidation"
@ -126,16 +134,17 @@
</template> </template>
<script> <script>
import { SEND_TYPES } from '@/pages/Send' import { SEND_TYPES } from '@/pages/Send'
import InputEmail from '@/components/Inputs/InputEmail' import InputIdentifier from '@/components/Inputs/InputIdentifier'
import InputAmount from '@/components/Inputs/InputAmount' import InputAmount from '@/components/Inputs/InputAmount'
import InputTextarea from '@/components/Inputs/InputTextarea' import InputTextarea from '@/components/Inputs/InputTextarea'
import { user as userQuery } from '@/graphql/queries' import { user as userQuery } from '@/graphql/queries'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import { COMMUNITY_NAME } from '@/config'
export default { export default {
name: 'TransactionForm', name: 'TransactionForm',
components: { components: {
InputEmail, InputIdentifier,
InputAmount, InputAmount,
InputTextarea, InputTextarea,
}, },
@ -155,6 +164,7 @@ export default {
}, },
radioSelected: this.selected, radioSelected: this.selected,
userName: '', userName: '',
communityName: COMMUNITY_NAME,
} }
}, },
methods: { methods: {

View File

@ -0,0 +1,68 @@
<template>
<validation-provider
tag="div"
:rules="rules"
:name="name"
v-slot="{ errors, valid, validated, ariaInput, ariaMsg }"
>
<b-form-group :label="label" :label-for="labelFor" data-test="input-identifier">
<b-form-input
v-model="currentValue"
v-bind="ariaInput"
:id="labelFor"
:name="name"
:placeholder="placeholder"
type="text"
:state="validated ? valid : false"
trim
class="bg-248"
:disabled="disabled"
autocomplete="off"
></b-form-input>
<b-form-invalid-feedback v-bind="ariaMsg">
{{ errors[0] }}
</b-form-invalid-feedback>
</b-form-group>
</validation-provider>
</template>
<script>
export default {
name: 'InputEmail',
props: {
rules: {
default: () => {
return {
required: true,
validIdentifier: true,
}
},
},
name: { type: String, required: true },
label: { type: String, required: true },
placeholder: { type: String, required: true },
value: { type: String, required: true },
disabled: { type: Boolean, required: false, default: false },
},
data() {
return {
currentValue: this.value,
}
},
computed: {
labelFor() {
return this.name + '-input-field'
},
},
watch: {
currentValue() {
this.$emit('input', this.currentValue)
},
value() {
if (this.value !== this.currentValue) {
this.currentValue = this.value
}
this.$emit('onValidation')
},
},
}
</script>

View File

@ -8,7 +8,7 @@
containsLowercaseCharacter: true, containsLowercaseCharacter: true,
containsUppercaseCharacter: true, containsUppercaseCharacter: true,
containsNumericCharacter: true, containsNumericCharacter: true,
atLeastEightCharactera: true, atLeastEightCharacters: true,
atLeastOneSpecialCharater: true, atLeastOneSpecialCharater: true,
noWhitespaceCharacters: true, noWhitespaceCharacters: true,
}" }"

Some files were not shown because too many files have changed in this diff Show More