mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into 2892-Frontend-multiple-auto-logout-messages
This commit is contained in:
commit
72f101ecc2
41
CHANGELOG.md
41
CHANGELOG.md
@ -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).
|
||||
|
||||
#### [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)
|
||||
|
||||
> 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(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)
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
coverage/
|
||||
2
admin/.gitignore
vendored
2
admin/.gitignore
vendored
@ -1,5 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
.cache/
|
||||
|
||||
/.env
|
||||
|
||||
@ -84,7 +84,7 @@ CMD /bin/sh -c "yarn run dev"
|
||||
FROM base as production
|
||||
|
||||
# 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
|
||||
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
|
||||
# Copy static files
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"description": "Administraion Interface for Gradido",
|
||||
"main": "index.js",
|
||||
"author": "Moriz Wahl",
|
||||
"version": "1.20.0",
|
||||
"version": "1.21.0",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
@ -11,7 +11,7 @@
|
||||
"serve": "vue-cli-service serve --open",
|
||||
"build": "vue-cli-service build",
|
||||
"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 .",
|
||||
"stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'",
|
||||
"test": "cross-env TZ=UTC jest",
|
||||
|
||||
@ -9,10 +9,10 @@ const port = process.env.PORT || 8080
|
||||
// Express Server
|
||||
const app = express()
|
||||
// Serve files
|
||||
app.use(express.static(path.join(__dirname, '../dist')))
|
||||
app.use(express.static(path.join(__dirname, '../build')))
|
||||
// Default to index.html
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../dist/index.html'))
|
||||
res.sendFile(path.join(__dirname, '../build/index.html'))
|
||||
})
|
||||
|
||||
app.listen(port, hostname, () => {
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
</template>
|
||||
<script>
|
||||
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 }
|
||||
|
||||
|
||||
@ -37,6 +37,7 @@ export const actions = {
|
||||
const store = new Vuex.Store({
|
||||
plugins: [
|
||||
createPersistedState({
|
||||
key: 'gradido-admin',
|
||||
storage: window.localStorage,
|
||||
}),
|
||||
],
|
||||
|
||||
@ -49,5 +49,5 @@ module.exports = {
|
||||
// Enable CSS source maps.
|
||||
sourceMap: CONFIG.NODE_ENV !== 'production',
|
||||
},
|
||||
outputDir: path.resolve(__dirname, './dist'),
|
||||
outputDir: path.resolve(__dirname, './build'),
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
node_modules
|
||||
**/*.min.js
|
||||
build
|
||||
build
|
||||
coverage
|
||||
@ -12,6 +12,8 @@ module.exports = {
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:import/recommended',
|
||||
'plugin:import/typescript',
|
||||
'plugin:security/recommended',
|
||||
'plugin:@eslint-community/eslint-comments/recommended',
|
||||
],
|
||||
settings: {
|
||||
'import/parsers': {
|
||||
@ -25,7 +27,8 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-console': ['error'],
|
||||
'no-console': 'error',
|
||||
camelcase: ['error', { allow: ['FederationClient_*'] }],
|
||||
'no-debugger': 'error',
|
||||
'prettier/prettier': [
|
||||
'error',
|
||||
@ -151,6 +154,11 @@ module.exports = {
|
||||
'promise/valid-params': 'warn',
|
||||
'promise/prefer-await-to-callbacks': '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: [
|
||||
// only for ts files
|
||||
@ -159,6 +167,7 @@ module.exports = {
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||
'plugin:@typescript-eslint/strict',
|
||||
'plugin:type-graphql/recommended',
|
||||
],
|
||||
rules: {
|
||||
@ -169,11 +178,14 @@ module.exports = {
|
||||
'@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
|
||||
'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: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json', '**/tsconfig.json'],
|
||||
// this is to properly reference the referenced project database without requirement of compiling it
|
||||
// eslint-disable-next-line camelcase
|
||||
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
|
||||
},
|
||||
},
|
||||
|
||||
@ -7,7 +7,7 @@ module.exports = {
|
||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 85,
|
||||
lines: 89,
|
||||
},
|
||||
},
|
||||
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-backend",
|
||||
"version": "1.20.0",
|
||||
"version": "1.21.0",
|
||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||
"main": "src/index.ts",
|
||||
"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/",
|
||||
"clean": "tsc --build --clean",
|
||||
"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 .",
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -33,7 +33,7 @@
|
||||
"graphql": "^15.5.1",
|
||||
"graphql-request": "5.0.0",
|
||||
"i18n": "^0.15.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jose": "^4.14.4",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"log4js": "^6.4.6",
|
||||
"mysql2": "^2.3.0",
|
||||
@ -46,12 +46,12 @@
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "^3.2.1",
|
||||
"@types/email-templates": "^10.0.1",
|
||||
"@types/express": "^4.17.12",
|
||||
"@types/faker": "^5.5.9",
|
||||
"@types/i18n": "^0.13.4",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/jsonwebtoken": "^8.5.2",
|
||||
"@types/lodash.clonedeep": "^4.5.6",
|
||||
"@types/node": "^16.10.3",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
@ -68,6 +68,7 @@
|
||||
"eslint-plugin-n": "^15.7.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-security": "^1.7.1",
|
||||
"eslint-plugin-type-graphql": "^1.0.0",
|
||||
"faker": "^5.5.3",
|
||||
"graphql-tag": "^2.12.6",
|
||||
|
||||
@ -7,7 +7,6 @@ import axios from 'axios'
|
||||
import { LogError } from '@/server/LogError'
|
||||
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> => {
|
||||
logger.trace('POST', url, payload)
|
||||
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> => {
|
||||
logger.trace('GET: url=' + url)
|
||||
try {
|
||||
|
||||
@ -4,15 +4,15 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
// eslint-disable-next-line import/no-relative-parent-imports
|
||||
import KlicktippConnector from 'klicktipp-api'
|
||||
|
||||
const klicktippConnector = new KlicktippConnector()
|
||||
|
||||
export const klicktippSignIn = async (
|
||||
export const subscribe = async (
|
||||
email: string,
|
||||
language: string,
|
||||
firstName?: string,
|
||||
@ -28,13 +28,6 @@ export const klicktippSignIn = async (
|
||||
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> => {
|
||||
if (!CONFIG.KLICKTIPP) return true
|
||||
const isLogin = await loginKlicktippUser()
|
||||
@ -48,9 +41,12 @@ export const getKlickTippUser = async (email: string): Promise<any> => {
|
||||
if (!CONFIG.KLICKTIPP) return true
|
||||
const isLogin = await loginKlicktippUser()
|
||||
if (isLogin) {
|
||||
const subscriberId = await klicktippConnector.subscriberSearch(email)
|
||||
const result = await klicktippConnector.subscriberGet(subscriberId)
|
||||
return result
|
||||
try {
|
||||
return klicktippConnector.subscriberGet(await klicktippConnector.subscriberSearch(email))
|
||||
} catch (e) {
|
||||
logger.error('Could not find subscriber', email)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -60,38 +56,6 @@ export const loginKlicktippUser = async (): Promise<boolean> => {
|
||||
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 (
|
||||
email: string,
|
||||
fields: any = {},
|
||||
@ -101,8 +65,18 @@ export const addFieldsToSubscriber = async (
|
||||
if (!CONFIG.KLICKTIPP) return true
|
||||
const isLogin = await loginKlicktippUser()
|
||||
if (isLogin) {
|
||||
const subscriberId = await klicktippConnector.subscriberSearch(email)
|
||||
return klicktippConnector.subscriberUpdate(subscriberId, fields, newemail, newsmsnumber)
|
||||
try {
|
||||
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
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { JwtPayload } from 'jsonwebtoken'
|
||||
import { JWTPayload } from 'jose'
|
||||
|
||||
export interface CustomJwtPayload extends JwtPayload {
|
||||
export interface CustomJwtPayload extends JWTPayload {
|
||||
gradidoID: string
|
||||
}
|
||||
|
||||
@ -8,4 +8,5 @@ export const INALIENABLE_RIGHTS = [
|
||||
RIGHTS.SET_PASSWORD,
|
||||
RIGHTS.QUERY_TRANSACTION_LINK,
|
||||
RIGHTS.QUERY_OPT_IN,
|
||||
RIGHTS.CHECK_USERNAME,
|
||||
]
|
||||
|
||||
@ -1,22 +1,33 @@
|
||||
import { verify, sign } from 'jsonwebtoken'
|
||||
import { SignJWT, jwtVerify } from 'jose'
|
||||
|
||||
import { CONFIG } from '@/config/'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
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')
|
||||
|
||||
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) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const encode = (gradidoID: string): string => {
|
||||
const token = sign({ gradidoID }, CONFIG.JWT_SECRET, {
|
||||
expiresIn: CONFIG.JWT_EXPIRES_IN,
|
||||
})
|
||||
export const encode = async (gradidoID: string): Promise<string> => {
|
||||
const secret = new TextEncoder().encode(CONFIG.JWT_SECRET)
|
||||
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
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@ export enum RIGHTS {
|
||||
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
|
||||
OPEN_CREATIONS = 'OPEN_CREATIONS',
|
||||
USER = 'USER',
|
||||
CHECK_USERNAME = 'CHECK_USERNAME',
|
||||
// Admin
|
||||
SEARCH_USERS = 'SEARCH_USERS',
|
||||
SET_USER_ROLE = 'SET_USER_ROLE',
|
||||
|
||||
@ -12,11 +12,11 @@ Decimal.set({
|
||||
})
|
||||
|
||||
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
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v15.2023-02-07',
|
||||
@ -25,67 +25,67 @@ const constants = {
|
||||
}
|
||||
|
||||
const server = {
|
||||
PORT: process.env.PORT || 4000,
|
||||
JWT_SECRET: process.env.JWT_SECRET || 'secret123',
|
||||
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m',
|
||||
PORT: process.env.PORT ?? 4000,
|
||||
JWT_SECRET: process.env.JWT_SECRET ?? 'secret123',
|
||||
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN ?? '10m',
|
||||
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,
|
||||
}
|
||||
|
||||
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_USER: process.env.DB_USER || 'root',
|
||||
DB_PASSWORD: process.env.DB_PASSWORD || '',
|
||||
DB_DATABASE: process.env.DB_DATABASE || 'gradido_community',
|
||||
TYPEORM_LOGGING_RELATIVE_PATH: process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.backend.log',
|
||||
DB_USER: process.env.DB_USER ?? 'root',
|
||||
DB_PASSWORD: process.env.DB_PASSWORD ?? '',
|
||||
DB_DATABASE: process.env.DB_DATABASE ?? 'gradido_community',
|
||||
TYPEORM_LOGGING_RELATIVE_PATH: process.env.TYPEORM_LOGGING_RELATIVE_PATH ?? 'typeorm.backend.log',
|
||||
}
|
||||
|
||||
const klicktipp = {
|
||||
KLICKTIPP: process.env.KLICKTIPP === 'true' || false,
|
||||
KLICKTTIPP_API_URL: process.env.KLICKTIPP_API_URL || 'https://api.klicktipp.com',
|
||||
KLICKTIPP_USER: process.env.KLICKTIPP_USER || 'gradido_test',
|
||||
KLICKTIPP_PASSWORD: process.env.KLICKTIPP_PASSWORD || 'secret321',
|
||||
KLICKTIPP_APIKEY_DE: process.env.KLICKTIPP_APIKEY_DE || 'SomeFakeKeyDE',
|
||||
KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN || 'SomeFakeKeyEN',
|
||||
KLICKTTIPP_API_URL: process.env.KLICKTIPP_API_URL ?? 'https://api.klicktipp.com',
|
||||
KLICKTIPP_USER: process.env.KLICKTIPP_USER ?? 'gradido_test',
|
||||
KLICKTIPP_PASSWORD: process.env.KLICKTIPP_PASSWORD ?? 'secret321',
|
||||
KLICKTIPP_APIKEY_DE: process.env.KLICKTIPP_APIKEY_DE ?? 'SomeFakeKeyDE',
|
||||
KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN ?? 'SomeFakeKeyEN',
|
||||
}
|
||||
|
||||
const community = {
|
||||
COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung',
|
||||
COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/',
|
||||
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register',
|
||||
COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL || 'http://localhost/redeem/{code}',
|
||||
COMMUNITY_NAME: process.env.COMMUNITY_NAME ?? 'Gradido Entwicklung',
|
||||
COMMUNITY_URL: process.env.COMMUNITY_URL ?? 'http://localhost/',
|
||||
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL ?? 'http://localhost/register',
|
||||
COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL ?? 'http://localhost/redeem/{code}',
|
||||
COMMUNITY_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:
|
||||
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
|
||||
COMMUNITY_SUPPORT_MAIL: process.env.COMMUNITY_SUPPORT_MAIL || 'support@supportmail.com',
|
||||
process.env.COMMUNITY_DESCRIPTION ?? 'Die lokale Entwicklungsumgebung von Gradido.',
|
||||
COMMUNITY_SUPPORT_MAIL: process.env.COMMUNITY_SUPPORT_MAIL ?? 'support@supportmail.com',
|
||||
}
|
||||
|
||||
const loginServer = {
|
||||
LOGIN_APP_SECRET: process.env.LOGIN_APP_SECRET || '21ffbbc616fe',
|
||||
LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY || 'a51ef8ac7ef1abf162fb7a65261acd7a',
|
||||
LOGIN_APP_SECRET: process.env.LOGIN_APP_SECRET ?? '21ffbbc616fe',
|
||||
LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY ?? 'a51ef8ac7ef1abf162fb7a65261acd7a',
|
||||
}
|
||||
|
||||
const email = {
|
||||
EMAIL: process.env.EMAIL === 'true' || false,
|
||||
EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' || false,
|
||||
EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER || 'stage1@gradido.net',
|
||||
EMAIL_USERNAME: process.env.EMAIL_USERNAME || '',
|
||||
EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net',
|
||||
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || '',
|
||||
EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'mailserver',
|
||||
EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER ?? 'stage1@gradido.net',
|
||||
EMAIL_USERNAME: process.env.EMAIL_USERNAME ?? '',
|
||||
EMAIL_SENDER: process.env.EMAIL_SENDER ?? 'info@gradido.net',
|
||||
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD ?? '',
|
||||
EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL ?? 'mailserver',
|
||||
EMAIL_SMTP_PORT: Number(process.env.EMAIL_SMTP_PORT) || 1025,
|
||||
// eslint-disable-next-line no-unneeded-ternary
|
||||
EMAIL_TLS: process.env.EMAIL_TLS === 'false' ? false : true,
|
||||
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:
|
||||
process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{optin}',
|
||||
process.env.EMAIL_LINK_SETPASSWORD ?? 'http://localhost/reset-password/{optin}',
|
||||
EMAIL_LINK_FORGOTPASSWORD:
|
||||
process.env.EMAIL_LINK_FORGOTPASSWORD || 'http://localhost/forgot-password',
|
||||
EMAIL_LINK_OVERVIEW: process.env.EMAIL_LINK_OVERVIEW || 'http://localhost/overview',
|
||||
process.env.EMAIL_LINK_FORGOTPASSWORD ?? 'http://localhost/forgot-password',
|
||||
EMAIL_LINK_OVERVIEW: process.env.EMAIL_LINK_OVERVIEW ?? 'http://localhost/overview',
|
||||
// time in minutes a optin code is valid
|
||||
EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME
|
||||
? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 1440
|
||||
@ -98,14 +98,14 @@ const email = {
|
||||
|
||||
const webhook = {
|
||||
// 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
|
||||
process.env.APP_SECRET = server.JWT_SECRET
|
||||
|
||||
// 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 (
|
||||
![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes(
|
||||
constants.CONFIG_VERSION.CURRENT,
|
||||
|
||||
@ -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 { gql } from 'graphql-request'
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
|
||||
import { GraphQLGetClient } from '@/federation/client/GraphQLGetClient'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { getPublicKey } from '@/federation/client/1_0/query/getPublicKey'
|
||||
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}'...`)
|
||||
// eslint-disable-next-line camelcase
|
||||
export class FederationClient {
|
||||
dbCom: DbFederatedCommunity
|
||||
endpoint: string
|
||||
client: GraphQLClient
|
||||
|
||||
const graphQLClient = GraphQLGetClient.getInstance(endpoint)
|
||||
logger.debug(`graphQLClient=${JSON.stringify(graphQLClient)}`)
|
||||
const query = gql`
|
||||
query {
|
||||
getPublicKey {
|
||||
publicKey
|
||||
constructor(dbCom: DbFederatedCommunity) {
|
||||
this.dbCom = dbCom
|
||||
this.endpoint = `${dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'}${
|
||||
dbCom.apiVersion
|
||||
}/`
|
||||
this.client = new GraphQLClient(this.endpoint, {
|
||||
method: 'GET',
|
||||
jsonSerializer: {
|
||||
parse: JSON.parse,
|
||||
stringify: JSON.stringify,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
getPublicKey = async (): Promise<string | undefined> => {
|
||||
logger.info('Federation: getPublicKey from endpoint', this.endpoint)
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const { data } = await this.client.rawRequest(getPublicKey, {})
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (!data?.getPublicKey?.publicKey) {
|
||||
logger.warn('Federation: getPublicKey without response data from endpoint', this.endpoint)
|
||||
return
|
||||
}
|
||||
}
|
||||
`
|
||||
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`)
|
||||
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) {
|
||||
logger.warn('Federation: getPublicKey failed for endpoint', this.endpoint)
|
||||
}
|
||||
logger.warn(`requestGetPublicKey processed without response data`)
|
||||
} catch (err) {
|
||||
throw new LogError(`Request-Error:`, err)
|
||||
}
|
||||
}
|
||||
|
||||
9
backend/src/federation/client/1_0/query/getPublicKey.ts
Normal file
9
backend/src/federation/client/1_0/query/getPublicKey.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { gql } from 'graphql-request'
|
||||
|
||||
export const getPublicKey = gql`
|
||||
query {
|
||||
getPublicKey {
|
||||
publicKey
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -1,44 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||
import { gql } from 'graphql-request'
|
||||
// eslint-disable-next-line camelcase
|
||||
import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient'
|
||||
|
||||
import { GraphQLGetClient } from '@/federation/client/GraphQLGetClient'
|
||||
import { LogError } from '@/server/LogError'
|
||||
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)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line camelcase
|
||||
export class FederationClient extends V1_0_FederationClient {}
|
||||
|
||||
62
backend/src/federation/client/FederationClientFactory.ts
Normal file
62
backend/src/federation/client/FederationClientFactory.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,8 @@
|
||||
import { Connection } from '@dbTools/typeorm'
|
||||
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||
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 { logger } from '@test/testSetup'
|
||||
@ -57,10 +59,23 @@ describe('validate Communities', () => {
|
||||
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 () => {
|
||||
// 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 = {
|
||||
publicKey: Buffer.from('11111111111111111111111111111111'),
|
||||
publicKey: Buffer.from(
|
||||
'1111111111111111111111111111111111111111111111111111111111111111',
|
||||
),
|
||||
apiVersion: '1_0',
|
||||
endPoint: 'http//localhost:5001/api/',
|
||||
lastAnnouncedAt: new Date(),
|
||||
@ -70,6 +85,7 @@ describe('validate Communities', () => {
|
||||
.into(DbFederatedCommunity)
|
||||
.values(variables1)
|
||||
.orUpdate({
|
||||
// eslint-disable-next-line camelcase
|
||||
conflict_target: ['id', 'publicKey', 'apiVersion'],
|
||||
overwrite: ['end_point', 'last_announced_at'],
|
||||
})
|
||||
@ -84,14 +100,89 @@ describe('validate Communities', () => {
|
||||
})
|
||||
it('logs requestGetPublicKey for community api 1_0 ', () => {
|
||||
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', () => {
|
||||
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 = {
|
||||
publicKey: Buffer.from('11111111111111111111111111111111'),
|
||||
publicKey: Buffer.from(
|
||||
'1111111111111111111111111111111111111111111111111111111111111111',
|
||||
),
|
||||
apiVersion: '1_1',
|
||||
endPoint: 'http//localhost:5001/api/',
|
||||
lastAnnouncedAt: new Date(),
|
||||
@ -101,11 +192,13 @@ describe('validate Communities', () => {
|
||||
.into(DbFederatedCommunity)
|
||||
.values(variables2)
|
||||
.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()
|
||||
})
|
||||
@ -114,12 +207,14 @@ describe('validate Communities', () => {
|
||||
})
|
||||
it('logs requestGetPublicKey for community api 1_0 ', () => {
|
||||
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 ', () => {
|
||||
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
|
||||
beforeEach(async () => {
|
||||
const variables3 = {
|
||||
publicKey: Buffer.from('11111111111111111111111111111111'),
|
||||
publicKey: Buffer.from(
|
||||
'1111111111111111111111111111111111111111111111111111111111111111',
|
||||
),
|
||||
apiVersion: '2_0',
|
||||
endPoint: 'http//localhost:5001/api/',
|
||||
lastAnnouncedAt: new Date(),
|
||||
@ -137,6 +234,7 @@ describe('validate Communities', () => {
|
||||
.into(DbFederatedCommunity)
|
||||
.values(variables3)
|
||||
.orUpdate({
|
||||
// eslint-disable-next-line camelcase
|
||||
conflict_target: ['id', 'publicKey', 'apiVersion'],
|
||||
overwrite: ['end_point', 'last_announced_at'],
|
||||
})
|
||||
@ -144,6 +242,7 @@ describe('validate Communities', () => {
|
||||
dbCom = await DbFederatedCommunity.findOneOrFail({
|
||||
where: { publicKey: variables3.publicKey, apiVersion: variables3.apiVersion },
|
||||
})
|
||||
await DbFederatedCommunity.update({}, { verifiedAt: null })
|
||||
jest.clearAllMocks()
|
||||
await validateCommunities()
|
||||
})
|
||||
@ -152,18 +251,21 @@ describe('validate Communities', () => {
|
||||
})
|
||||
it('logs requestGetPublicKey for community api 1_0 ', () => {
|
||||
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 ', () => {
|
||||
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 ', () => {
|
||||
expect(logger.warn).toBeCalledWith(
|
||||
`Federation: dbCom: ${dbCom.id} with unsupported apiVersion=2_0; supported versions`,
|
||||
['1_0', '1_1'],
|
||||
'Federation: dbCom with unsupported apiVersion',
|
||||
dbCom.endPoint,
|
||||
'2_0',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -3,13 +3,11 @@
|
||||
import { IsNull } from '@dbTools/typeorm'
|
||||
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'
|
||||
|
||||
// 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'
|
||||
|
||||
export function startValidateCommunities(timerInterval: number): void {
|
||||
@ -36,56 +34,28 @@ export async function validateCommunities(): Promise<void> {
|
||||
logger.debug('Federation: dbCom', dbCom)
|
||||
const apiValueStrings: string[] = Object.values(ApiVersionType)
|
||||
logger.debug(`suppported ApiVersions=`, apiValueStrings)
|
||||
if (apiValueStrings.includes(dbCom.apiVersion)) {
|
||||
logger.debug(
|
||||
`Federation: validate publicKey for dbCom: ${dbCom.id} with apiVersion=${dbCom.apiVersion}`,
|
||||
)
|
||||
try {
|
||||
const pubKey = await invokeVersionedRequestGetPublicKey(dbCom)
|
||||
logger.info(
|
||||
'Federation: received publicKey from endpoint',
|
||||
pubKey,
|
||||
`${dbCom.endPoint}/${dbCom.apiVersion}`,
|
||||
)
|
||||
if (!apiValueStrings.includes(dbCom.apiVersion)) {
|
||||
logger.warn('Federation: dbCom with unsupported apiVersion', dbCom.endPoint, dbCom.apiVersion)
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const client = FederationClientFactory.getInstance(dbCom)
|
||||
// eslint-disable-next-line camelcase
|
||||
if (client instanceof V1_0_FederationClient) {
|
||||
const pubKey = await client.getPublicKey()
|
||||
if (pubKey && pubKey === dbCom.publicKey.toString()) {
|
||||
logger.info(`Federation: matching publicKey: ${pubKey}`)
|
||||
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 {
|
||||
logger.warn(
|
||||
`Federation: received not matching publicKey -> received: ${
|
||||
pubKey || 'null'
|
||||
}, expected: ${dbCom.publicKey.toString()} `,
|
||||
'Federation: received not matching publicKey:',
|
||||
pubKey,
|
||||
dbCom.publicKey.toString(),
|
||||
)
|
||||
// DbCommunity.delete({ id: dbCom.id })
|
||||
}
|
||||
} catch (err) {
|
||||
if (!isLogError(err)) {
|
||||
logger.error(`Error:`, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.warn(
|
||||
`Federation: dbCom: ${dbCom.id} with unsupported apiVersion=${dbCom.apiVersion}; supported versions`,
|
||||
apiValueStrings,
|
||||
)
|
||||
} catch (err) {
|
||||
logger.error(`Error:`, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,9 @@ export class UpdateUserInfosArgs {
|
||||
@Field({ nullable: true })
|
||||
lastName?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
alias?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
language?: string
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ export const isAuthorized: AuthChecker<Context> = async ({ context }, rights) =>
|
||||
context.role = ROLE_UNAUTHORIZED // unauthorized user
|
||||
|
||||
// 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
|
||||
|
||||
// Do we have a token?
|
||||
@ -21,7 +21,7 @@ export const isAuthorized: AuthChecker<Context> = async ({ context }, rights) =>
|
||||
}
|
||||
|
||||
// Decode the token
|
||||
const decoded = decode(context.token)
|
||||
const decoded = await decode(context.token)
|
||||
if (!decoded) {
|
||||
throw new LogError('403.13 - Client certificate revoked')
|
||||
}
|
||||
@ -43,12 +43,12 @@ export const isAuthorized: AuthChecker<Context> = async ({ context }, 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) {
|
||||
throw new LogError('401 Unauthorized')
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ export class Balance {
|
||||
linkCount: number
|
||||
}) {
|
||||
this.balance = data.balance
|
||||
this.balanceGDT = data.balanceGDT || null
|
||||
this.balanceGDT = data.balanceGDT ?? null
|
||||
this.count = data.count
|
||||
this.linkCount = data.linkCount
|
||||
}
|
||||
|
||||
@ -43,13 +43,12 @@ export class Transaction {
|
||||
this.memo = transaction.memo
|
||||
this.creationDate = transaction.creationDate
|
||||
this.linkedUser = linkedUser
|
||||
this.linkedTransactionId = transaction.linkedTransactionId || null
|
||||
this.linkedTransactionId = transaction.linkedTransactionId ?? null
|
||||
this.linkId = transaction.contribution
|
||||
? transaction.contribution.contributionLinkId
|
||||
: transaction.transactionLinkId || null
|
||||
: transaction.transactionLinkId ?? null
|
||||
this.previousBalance =
|
||||
(transaction.previousTransaction &&
|
||||
transaction.previousTransaction.balance.toDecimalPlaces(2, Decimal.ROUND_DOWN)) ||
|
||||
transaction.previousTransaction?.balance.toDecimalPlaces(2, Decimal.ROUND_DOWN) ??
|
||||
new Decimal(0)
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@ import { User as dbUser } from '@entity/User'
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
|
||||
import { KlickTipp } from './KlickTipp'
|
||||
import { UserContact } from './UserContact'
|
||||
|
||||
@ObjectType()
|
||||
export class User {
|
||||
@ -10,10 +9,7 @@ export class User {
|
||||
this.id = user.id
|
||||
this.gradidoID = user.gradidoID
|
||||
this.alias = user.alias
|
||||
this.emailId = user.emailId
|
||||
if (user.emailContact) {
|
||||
this.email = user.emailContact.email
|
||||
this.emailContact = new UserContact(user.emailContact)
|
||||
this.emailChecked = user.emailContact.emailChecked
|
||||
}
|
||||
this.firstName = user.firstName
|
||||
@ -38,16 +34,6 @@ export class User {
|
||||
@Field(() => String, { nullable: true })
|
||||
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 })
|
||||
firstName: string | null
|
||||
|
||||
|
||||
@ -70,7 +70,10 @@ export class BalanceResolver {
|
||||
now,
|
||||
)
|
||||
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
|
||||
@ -96,9 +99,7 @@ export class BalanceResolver {
|
||||
count,
|
||||
linkCount,
|
||||
})
|
||||
logger.info(
|
||||
`new Balance(balance=${balance}, balanceGDT=${balanceGDT}, count=${count}, linkCount=${linkCount}) = ${newBalance}`,
|
||||
)
|
||||
logger.info('new Balance', balance, balanceGDT, count, linkCount, newBalance)
|
||||
|
||||
return newBalance
|
||||
}
|
||||
|
||||
@ -66,7 +66,7 @@ let testEnv: {
|
||||
query: ApolloServerTestClient['query']
|
||||
con: Connection
|
||||
}
|
||||
let creation: Contribution | void
|
||||
let creation: Contribution | null
|
||||
let admin: User
|
||||
let pendingContribution: any
|
||||
let inProgressContribution: any
|
||||
@ -2071,7 +2071,7 @@ describe('ContributionResolver', () => {
|
||||
mutate({
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
contributionId: (adminContribution && adminContribution.id) || -1,
|
||||
contributionId: adminContribution?.id ?? -1,
|
||||
amount: 100.0,
|
||||
memo: 'Test Test Test',
|
||||
creationDate: new Date().toString(),
|
||||
@ -2565,8 +2565,8 @@ describe('ContributionResolver', () => {
|
||||
})
|
||||
|
||||
describe('confirm two creations one after the other quickly', () => {
|
||||
let c1: Contribution | void
|
||||
let c2: Contribution | void
|
||||
let c1: Contribution | null
|
||||
let c2: Contribution | null
|
||||
|
||||
beforeAll(async () => {
|
||||
const now = new Date()
|
||||
|
||||
@ -43,6 +43,7 @@ import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { calculateDecay } from '@/util/decay'
|
||||
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||
import { fullName } from '@/util/utilities'
|
||||
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||
import {
|
||||
@ -269,7 +270,7 @@ export class ContributionResolver {
|
||||
withDeleted: true,
|
||||
relations: ['user'],
|
||||
})
|
||||
if (!emailContact || !emailContact.user) {
|
||||
if (!emailContact?.user) {
|
||||
throw new LogError('Could not find user', email)
|
||||
}
|
||||
if (emailContact.deletedAt || emailContact.user.deletedAt) {
|
||||
@ -500,6 +501,8 @@ export class ContributionResolver {
|
||||
transaction.typeId = TransactionTypeId.CREATION
|
||||
transaction.memo = contribution.memo
|
||||
transaction.userId = contribution.userId
|
||||
transaction.userGradidoID = user.gradidoID
|
||||
transaction.userName = fullName(user.firstName, user.lastName)
|
||||
transaction.previous = lastTransaction ? lastTransaction.id : null
|
||||
transaction.amount = contribution.amount
|
||||
transaction.creationDate = contribution.contributionDate
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
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 { EVENT_NEWSLETTER_SUBSCRIBE, EVENT_NEWSLETTER_UNSUBSCRIBE } from '@/event/Events'
|
||||
import { Context, getUser } from '@/server/context'
|
||||
@ -20,6 +20,6 @@ export class KlicktippResolver {
|
||||
async subscribeNewsletter(@Ctx() context: Context): Promise<boolean> {
|
||||
const user = getUser(context)
|
||||
await EVENT_NEWSLETTER_SUBSCRIBE(user)
|
||||
return klicktippSignIn(user.emailContact.email, user.language)
|
||||
return subscribe(user.emailContact.email, user.language)
|
||||
}
|
||||
}
|
||||
|
||||
@ -817,8 +817,8 @@ describe('TransactionLinkResolver', () => {
|
||||
const bibisTransaktionLinks = transactionLinks.filter(
|
||||
(transactionLink) => transactionLink.email === 'bibi@bloxberg.de',
|
||||
)
|
||||
for (let i = 0; i < bibisTransaktionLinks.length; i++) {
|
||||
await transactionLinkFactory(testEnv, bibisTransaktionLinks[i])
|
||||
for (const bibisTransaktionLink of bibisTransaktionLinks) {
|
||||
await transactionLinkFactory(testEnv, bibisTransaktionLink)
|
||||
}
|
||||
|
||||
// admin: only now log in
|
||||
@ -1040,6 +1040,7 @@ describe('TransactionLinkResolver', () => {
|
||||
})
|
||||
|
||||
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) + '$')
|
||||
expect(transactionLinkCode(date)).toEqual(expect.stringMatching(regexp))
|
||||
})
|
||||
|
||||
@ -34,6 +34,7 @@ import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { calculateDecay } from '@/util/decay'
|
||||
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||
import { fullName } from '@/util/utilities'
|
||||
import { calculateBalance } from '@/util/validate'
|
||||
|
||||
import { executeTransaction } from './TransactionResolver'
|
||||
@ -146,7 +147,7 @@ export class TransactionLinkResolver {
|
||||
const transactionLink = await DbTransactionLink.findOneOrFail({ code }, { withDeleted: true })
|
||||
const user = await DbUser.findOneOrFail({ id: transactionLink.userId })
|
||||
let redeemedBy: User | null = null
|
||||
if (transactionLink && transactionLink.redeemedBy) {
|
||||
if (transactionLink?.redeemedBy) {
|
||||
redeemedBy = new User(await DbUser.findOneOrFail({ id: transactionLink.redeemedBy }))
|
||||
}
|
||||
return new TransactionLink(transactionLink, new User(user), redeemedBy)
|
||||
@ -266,6 +267,8 @@ export class TransactionLinkResolver {
|
||||
transaction.typeId = TransactionTypeId.CREATION
|
||||
transaction.memo = contribution.memo
|
||||
transaction.userId = contribution.userId
|
||||
transaction.userGradidoID = user.gradidoID
|
||||
transaction.userName = fullName(user.firstName, user.lastName)
|
||||
transaction.previous = lastTransaction ? lastTransaction.id : null
|
||||
transaction.amount = contribution.amount
|
||||
transaction.creationDate = contribution.contributionDate
|
||||
|
||||
@ -19,13 +19,17 @@ import {
|
||||
createContribution,
|
||||
login,
|
||||
sendCoins,
|
||||
updateUserInfos,
|
||||
} from '@/seeds/graphql/mutations'
|
||||
import { transactionsQuery } from '@/seeds/graphql/queries'
|
||||
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||
import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { stephenHawking } from '@/seeds/users/stephen-hawking'
|
||||
|
||||
let mutate: ApolloServerTestClient['mutate'], con: Connection
|
||||
let query: ApolloServerTestClient['query']
|
||||
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
@ -35,6 +39,7 @@ let testEnv: {
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment(logger)
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
await cleanDB()
|
||||
})
|
||||
@ -48,10 +53,13 @@ let bobData: any
|
||||
let peterData: any
|
||||
let user: User[]
|
||||
|
||||
let bob: User
|
||||
let peter: User
|
||||
|
||||
describe('send coins', () => {
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, peterLustig)
|
||||
await userFactory(testEnv, bobBaumeister)
|
||||
peter = await userFactory(testEnv, peterLustig)
|
||||
bob = await userFactory(testEnv, bobBaumeister)
|
||||
await userFactory(testEnv, stephenHawking)
|
||||
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', () => {
|
||||
it('sends the coins four times in a row', async () => {
|
||||
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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -29,6 +29,7 @@ import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { communityUser } from '@/util/communityUser'
|
||||
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||
import { fullName } from '@/util/utilities'
|
||||
import { calculateBalance } from '@/util/validate'
|
||||
import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions'
|
||||
|
||||
@ -48,9 +49,7 @@ export const executeTransaction = async (
|
||||
// acquire lock
|
||||
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||
try {
|
||||
logger.info(
|
||||
`executeTransaction(amount=${amount}, memo=${memo}, sender=${sender}, recipient=${recipient})...`,
|
||||
)
|
||||
logger.info('executeTransaction', amount, memo, sender, recipient)
|
||||
|
||||
if (sender.id === recipient.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.memo = memo
|
||||
transactionSend.userId = sender.id
|
||||
transactionSend.userGradidoID = sender.gradidoID
|
||||
transactionSend.userName = fullName(sender.firstName, sender.lastName)
|
||||
transactionSend.linkedUserId = recipient.id
|
||||
transactionSend.linkedUserGradidoID = recipient.gradidoID
|
||||
transactionSend.linkedUserName = fullName(recipient.firstName, recipient.lastName)
|
||||
transactionSend.amount = amount.mul(-1)
|
||||
transactionSend.balance = sendBalance.balance
|
||||
transactionSend.balanceDate = receivedCallDate
|
||||
@ -103,7 +106,11 @@ export const executeTransaction = async (
|
||||
transactionReceive.typeId = TransactionTypeId.RECEIVE
|
||||
transactionReceive.memo = memo
|
||||
transactionReceive.userId = recipient.id
|
||||
transactionReceive.userGradidoID = recipient.gradidoID
|
||||
transactionReceive.userName = fullName(recipient.firstName, recipient.lastName)
|
||||
transactionReceive.linkedUserId = sender.id
|
||||
transactionReceive.linkedUserGradidoID = sender.gradidoID
|
||||
transactionReceive.linkedUserName = fullName(sender.firstName, sender.lastName)
|
||||
transactionReceive.amount = amount
|
||||
const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate)
|
||||
transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount
|
||||
@ -119,10 +126,10 @@ export const executeTransaction = async (
|
||||
// Save linked transaction id for send
|
||||
transactionSend.linkedTransactionId = transactionReceive.id
|
||||
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
|
||||
logger.debug(`send Transaction updated: ${transactionSend}`)
|
||||
logger.debug('send Transaction updated', transactionSend)
|
||||
|
||||
if (transactionLink) {
|
||||
logger.info(`transactionLink: ${transactionLink}`)
|
||||
logger.info('transactionLink', transactionLink)
|
||||
transactionLink.redeemedAt = receivedCallDate
|
||||
transactionLink.redeemedBy = recipient.id
|
||||
await queryRunner.manager.update(
|
||||
@ -271,8 +278,8 @@ export class TransactionResolver {
|
||||
sumAmount.mul(-1),
|
||||
sumHoldAvailableAmount.mul(-1),
|
||||
sumHoldAvailableAmount.minus(sumAmount.toString()).mul(-1),
|
||||
firstDate || now,
|
||||
lastDate || now,
|
||||
firstDate ?? now,
|
||||
lastDate ?? now,
|
||||
self,
|
||||
(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)
|
||||
}
|
||||
|
||||
// TODO this is subject to replay attacks
|
||||
const senderUser = getUser(context)
|
||||
|
||||
// validate recipient user
|
||||
@ -325,9 +331,7 @@ export class TransactionResolver {
|
||||
}
|
||||
|
||||
await executeTransaction(amount, memo, senderUser, recipientUser)
|
||||
logger.info(
|
||||
`successful executeTransaction(amount=${amount}, memo=${memo}, senderUser=${senderUser}, recipientUser=${recipientUser})`,
|
||||
)
|
||||
logger.info('successful executeTransaction', amount, memo, senderUser, recipientUser)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ import { ContributionLink } from '@model/ContributionLink'
|
||||
import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers'
|
||||
import { logger, i18n as localization } from '@test/testSetup'
|
||||
|
||||
import { subscribe } from '@/apis/KlicktippController'
|
||||
import { CONFIG } from '@/config'
|
||||
import {
|
||||
sendAccountActivationEmail,
|
||||
@ -52,6 +53,7 @@ import {
|
||||
searchAdminUsers,
|
||||
searchUsers,
|
||||
user as userQuery,
|
||||
checkUsername,
|
||||
} from '@/seeds/graphql/queries'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||
@ -61,8 +63,6 @@ import { stephenHawking } from '@/seeds/users/stephen-hawking'
|
||||
import { printTimeDuration } from '@/util/time'
|
||||
import { objectValuesToArray } from '@/util/utilities'
|
||||
|
||||
// import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||
|
||||
jest.mock('@/emails/sendEmailVariants', () => {
|
||||
const originalModule = jest.requireActual('@/emails/sendEmailVariants')
|
||||
return {
|
||||
@ -76,15 +76,13 @@ jest.mock('@/emails/sendEmailVariants', () => {
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
|
||||
jest.mock('@/apis/KlicktippController', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
klicktippSignIn: jest.fn(),
|
||||
subscribe: jest.fn(),
|
||||
getKlickTippUser: jest.fn(),
|
||||
}
|
||||
})
|
||||
*/
|
||||
|
||||
let admin: User
|
||||
let user: User
|
||||
@ -556,16 +554,14 @@ describe('UserResolver', () => {
|
||||
expect(newUser.password.toString()).toEqual(encryptedPass.toString())
|
||||
})
|
||||
|
||||
/*
|
||||
it('calls the klicktipp API', () => {
|
||||
expect(klicktippSignIn).toBeCalledWith(
|
||||
user[0].email,
|
||||
user[0].language,
|
||||
user[0].firstName,
|
||||
user[0].lastName,
|
||||
expect(subscribe).toBeCalledWith(
|
||||
newUser.emailContact.email,
|
||||
newUser.language,
|
||||
newUser.firstName,
|
||||
newUser.lastName,
|
||||
)
|
||||
})
|
||||
*/
|
||||
|
||||
it('returns true', () => {
|
||||
expect(result).toBeTruthy()
|
||||
@ -680,7 +676,6 @@ describe('UserResolver', () => {
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
login: {
|
||||
email: 'bibi@bloxberg.de',
|
||||
firstName: 'Bibi',
|
||||
hasElopage: false,
|
||||
id: expect.any(Number),
|
||||
@ -953,7 +948,6 @@ describe('UserResolver', () => {
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
verifyLogin: {
|
||||
email: 'bibi@bloxberg.de',
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
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', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
@ -1310,7 +1326,7 @@ describe('UserResolver', () => {
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
login: expect.objectContaining({
|
||||
email: 'bibi@bloxberg.de',
|
||||
firstName: 'Benjamin',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
@ -1457,7 +1473,6 @@ describe('UserResolver', () => {
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
login: {
|
||||
email: 'bibi@bloxberg.de',
|
||||
firstName: 'Bibi',
|
||||
hasElopage: false,
|
||||
id: expect.any(Number),
|
||||
@ -2343,15 +2358,21 @@ describe('UserResolver', () => {
|
||||
mutation: login,
|
||||
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 () => {
|
||||
await expect(
|
||||
query({
|
||||
query: userQuery,
|
||||
variables: {
|
||||
identifier: 'identifier',
|
||||
identifier: 'identifier_is_no_valid_alias!',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
@ -2359,7 +2380,10 @@ describe('UserResolver', () => {
|
||||
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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -35,7 +35,7 @@ import { User } from '@model/User'
|
||||
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
|
||||
import { UserRepository } from '@repository/User'
|
||||
|
||||
import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||
import { subscribe } from '@/apis/KlicktippController'
|
||||
import { encode } from '@/auth/JWT'
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { CONFIG } from '@/config'
|
||||
@ -73,6 +73,7 @@ import { getTimeDurationObject, printTimeDuration } from '@/util/time'
|
||||
import { FULL_CREATION_AVAILABLE } from './const/const'
|
||||
import { getUserCreations } from './util/creations'
|
||||
import { findUserByIdentifier } from './util/findUserByIdentifier'
|
||||
import { validateAlias } from './util/validateAlias'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs
|
||||
const random = require('random-bigint')
|
||||
@ -94,7 +95,7 @@ const newEmailContact = (email: string, userId: number): DbUserContact => {
|
||||
emailContact.emailChecked = false
|
||||
emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER
|
||||
emailContact.emailVerificationCode = random(64)
|
||||
logger.debug(`newEmailContact...successful: ${emailContact}`)
|
||||
logger.debug('newEmailContact...successful', emailContact)
|
||||
return emailContact
|
||||
}
|
||||
|
||||
@ -130,7 +131,7 @@ export class UserResolver {
|
||||
// Elopage Status & Stored PublisherId
|
||||
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
|
||||
}
|
||||
|
||||
@ -185,7 +186,7 @@ export class UserResolver {
|
||||
|
||||
context.setHeaders.push({
|
||||
key: 'token',
|
||||
value: encode(dbUser.gradidoID),
|
||||
value: await encode(dbUser.gradidoID),
|
||||
})
|
||||
|
||||
await EVENT_USER_LOGIN(dbUser)
|
||||
@ -225,7 +226,7 @@ export class UserResolver {
|
||||
email = email.trim().toLowerCase()
|
||||
if (await checkEmailExists(email)) {
|
||||
const foundUser = await findUserByEmail(email)
|
||||
logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`)
|
||||
logger.info('DbUser.findOne', email, foundUser)
|
||||
|
||||
if (foundUser) {
|
||||
// ATTENTION: this logger-message will be exactly expected during tests, next line
|
||||
@ -238,7 +239,6 @@ export class UserResolver {
|
||||
const user = new User(communityDbUser)
|
||||
user.id = sodium.randombytes_random() % (2048 * 16) // TODO: for a better faking derive id from email so that it will be always the same id when the same email comes in?
|
||||
user.gradidoID = uuidv4()
|
||||
user.email = email
|
||||
user.firstName = firstName
|
||||
user.lastName = lastName
|
||||
user.language = language
|
||||
@ -276,7 +276,7 @@ export class UserResolver {
|
||||
dbUser.firstName = firstName
|
||||
dbUser.lastName = lastName
|
||||
dbUser.language = language
|
||||
dbUser.publisherId = publisherId || 0
|
||||
dbUser.publisherId = publisherId ?? 0
|
||||
dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD
|
||||
logger.debug('new dbUser', dbUser)
|
||||
if (redeemCode) {
|
||||
@ -383,7 +383,7 @@ export class UserResolver {
|
||||
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({
|
||||
firstName: user.firstName,
|
||||
@ -469,9 +469,9 @@ export class UserResolver {
|
||||
// TODO do we always signUp the user? How to handle things with old users?
|
||||
if (userContact.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) {
|
||||
try {
|
||||
await klicktippSignIn(userContact.email, user.language, user.firstName, user.lastName)
|
||||
await subscribe(userContact.email, user.language, user.firstName, user.lastName)
|
||||
logger.debug(
|
||||
`klicktippSignIn(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
|
||||
`subscribe(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
|
||||
)
|
||||
} catch (e) {
|
||||
logger.error('Error subscribing to klicktipp', e)
|
||||
@ -487,7 +487,7 @@ export class UserResolver {
|
||||
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
|
||||
logger.info(`queryOptIn(${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
|
||||
if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) {
|
||||
throw new LogError(
|
||||
@ -498,6 +498,17 @@ export class UserResolver {
|
||||
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])
|
||||
@Mutation(() => Boolean)
|
||||
async updateUserInfos(
|
||||
@ -505,6 +516,7 @@ export class UserResolver {
|
||||
{
|
||||
firstName,
|
||||
lastName,
|
||||
alias,
|
||||
language,
|
||||
password,
|
||||
passwordNew,
|
||||
@ -524,6 +536,10 @@ export class UserResolver {
|
||||
user.lastName = lastName
|
||||
}
|
||||
|
||||
if (alias && (await validateAlias(alias))) {
|
||||
user.alias = alias
|
||||
}
|
||||
|
||||
if (language) {
|
||||
if (!isLanguage(language)) {
|
||||
throw new LogError('Given language is not a valid language', language)
|
||||
@ -587,7 +603,7 @@ export class UserResolver {
|
||||
logger.info(`hasElopage()...`)
|
||||
const userEntity = getUser(context)
|
||||
const elopageBuys = hasElopageBuys(userEntity.emailContact.email)
|
||||
logger.debug(`has ElopageBuys = ${elopageBuys}`)
|
||||
logger.debug('has ElopageBuys', elopageBuys)
|
||||
return elopageBuys
|
||||
}
|
||||
|
||||
@ -644,7 +660,7 @@ export class UserResolver {
|
||||
return 'user.' + fieldName
|
||||
}),
|
||||
searchText,
|
||||
filters || null,
|
||||
filters ?? null,
|
||||
currentPage,
|
||||
pageSize,
|
||||
)
|
||||
@ -710,14 +726,14 @@ export class UserResolver {
|
||||
// change isAdmin
|
||||
switch (user.isAdmin) {
|
||||
case null:
|
||||
if (isAdmin === true) {
|
||||
if (isAdmin) {
|
||||
user.isAdmin = new Date()
|
||||
} else {
|
||||
throw new LogError('User is already an usual user')
|
||||
}
|
||||
break
|
||||
default:
|
||||
if (isAdmin === false) {
|
||||
if (!isAdmin) {
|
||||
user.isAdmin = null
|
||||
} else {
|
||||
throw new LogError('User is already admin')
|
||||
|
||||
@ -29,10 +29,12 @@ export const validateContribution = (
|
||||
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())) {
|
||||
throw new LogError(
|
||||
'The amount to be created exceeds the amount still available for this month',
|
||||
amount,
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
creations[index],
|
||||
)
|
||||
}
|
||||
@ -151,6 +153,7 @@ export const updateCreations = (
|
||||
if (index < 0) {
|
||||
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())
|
||||
return creations
|
||||
}
|
||||
@ -169,6 +172,7 @@ export const getOpenCreations = async (
|
||||
return {
|
||||
month: date.getMonth(),
|
||||
year: date.getFullYear(),
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
amount: creations[index],
|
||||
}
|
||||
})
|
||||
|
||||
17
backend/src/graphql/resolver/util/eventList.ts
Normal file
17
backend/src/graphql/resolver/util/eventList.ts
Normal 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()
|
||||
}
|
||||
@ -24,7 +24,7 @@ export const findContributions = async (
|
||||
}
|
||||
return DbContribution.findAndCount({
|
||||
where: {
|
||||
...(statusFilter && statusFilter.length && { contributionStatus: In(statusFilter) }),
|
||||
...(statusFilter?.length && { contributionStatus: In(statusFilter) }),
|
||||
...(userId && { userId }),
|
||||
},
|
||||
withDeleted,
|
||||
|
||||
@ -4,6 +4,8 @@ import { validate, version } from 'uuid'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { VALID_ALIAS_REGEX } from './validateAlias'
|
||||
|
||||
export const findUserByIdentifier = async (identifier: string): Promise<DbUser> => {
|
||||
let user: DbUser | undefined
|
||||
if (validate(identifier) && version(identifier) === 4) {
|
||||
@ -27,8 +29,12 @@ export const findUserByIdentifier = async (identifier: string): Promise<DbUser>
|
||||
}
|
||||
user = userContact.user
|
||||
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 {
|
||||
// last is alias when implemented
|
||||
throw new LogError('Unknown identifier type', identifier)
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ export async function transactionLinkList(
|
||||
filters: TransactionLinkFilters | null,
|
||||
user: DbUser,
|
||||
): Promise<TransactionLinkResult> {
|
||||
const { withDeleted, withExpired, withRedeemed } = filters || {
|
||||
const { withDeleted, withExpired, withRedeemed } = filters ?? {
|
||||
withDeleted: false,
|
||||
withExpired: false,
|
||||
withRedeemed: false,
|
||||
|
||||
125
backend/src/graphql/resolver/util/validateAlias.test.ts
Normal file
125
backend/src/graphql/resolver/util/validateAlias.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
40
backend/src/graphql/resolver/util/validateAlias.ts
Normal file
40
backend/src/graphql/resolver/util/validateAlias.ts
Normal 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
|
||||
}
|
||||
@ -13,7 +13,7 @@ async function main() {
|
||||
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) => {
|
||||
|
||||
@ -10,19 +10,6 @@ import { KlickTipp } from '@model/KlickTipp'
|
||||
import { getKlickTippUser } from '@/apis/KlicktippController'
|
||||
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 (
|
||||
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
||||
{ root, args, context, info },
|
||||
|
||||
@ -28,6 +28,7 @@ export const updateUserInfos = gql`
|
||||
mutation (
|
||||
$firstName: String
|
||||
$lastName: String
|
||||
$alias: String
|
||||
$password: String
|
||||
$passwordNew: String
|
||||
$locale: String
|
||||
@ -37,6 +38,7 @@ export const updateUserInfos = gql`
|
||||
updateUserInfos(
|
||||
firstName: $firstName
|
||||
lastName: $lastName
|
||||
alias: $alias
|
||||
password: $password
|
||||
passwordNew: $passwordNew
|
||||
language: $locale
|
||||
@ -305,7 +307,6 @@ export const login = gql`
|
||||
mutation ($email: String!, $password: String!, $publisherId: Int) {
|
||||
login(email: $email, password: $password, publisherId: $publisherId) {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
language
|
||||
|
||||
@ -3,7 +3,6 @@ import { gql } from 'graphql-tag'
|
||||
export const verifyLogin = gql`
|
||||
query {
|
||||
verifyLogin {
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
language
|
||||
@ -23,32 +22,33 @@ export const queryOptIn = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const checkUsername = gql`
|
||||
query ($username: String!) {
|
||||
checkUsername(username: $username)
|
||||
}
|
||||
`
|
||||
|
||||
export const transactionsQuery = gql`
|
||||
query (
|
||||
$currentPage: Int = 1
|
||||
$pageSize: Int = 25
|
||||
$order: Order = DESC
|
||||
$onlyCreations: Boolean = false
|
||||
) {
|
||||
transactionList(
|
||||
currentPage: $currentPage
|
||||
pageSize: $pageSize
|
||||
order: $order
|
||||
onlyCreations: $onlyCreations
|
||||
) {
|
||||
balanceGDT
|
||||
count
|
||||
balance
|
||||
query ($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC) {
|
||||
transactionList(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
|
||||
balance {
|
||||
balance
|
||||
balanceGDT
|
||||
count
|
||||
linkCount
|
||||
}
|
||||
transactions {
|
||||
id
|
||||
typeId
|
||||
amount
|
||||
balance
|
||||
previousBalance
|
||||
balanceDate
|
||||
memo
|
||||
linkedUser {
|
||||
firstName
|
||||
lastName
|
||||
gradidoID
|
||||
}
|
||||
decay {
|
||||
decay
|
||||
@ -56,6 +56,7 @@ export const transactionsQuery = gql`
|
||||
end
|
||||
duration
|
||||
}
|
||||
linkId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,8 +31,8 @@ const context = {
|
||||
|
||||
export const cleanDB = async () => {
|
||||
// this only works as long we do not have foreign key constraints
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
await resetEntity(entities[i])
|
||||
for (const entity of entities) {
|
||||
await resetEntity(entity)
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,9 +54,8 @@ const run = async () => {
|
||||
logger.info('##seed## clean database successful...')
|
||||
|
||||
// seed the standard users
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
const dbUser = await userFactory(seedClient, users[i])
|
||||
logger.info(`##seed## seed standard users[ ${i} ]= ${JSON.stringify(dbUser, null, 2)}`)
|
||||
for (const user of users) {
|
||||
await userFactory(seedClient, user)
|
||||
}
|
||||
logger.info('##seed## seeding all standard users successful...')
|
||||
|
||||
@ -73,20 +72,20 @@ const run = async () => {
|
||||
logger.info('##seed## seeding all random users successful...')
|
||||
|
||||
// create GDD
|
||||
for (let i = 0; i < creations.length; i++) {
|
||||
await creationFactory(seedClient, creations[i])
|
||||
for (const creation of creations) {
|
||||
await creationFactory(seedClient, creation)
|
||||
}
|
||||
logger.info('##seed## seeding all creations successful...')
|
||||
|
||||
// create Transaction Links
|
||||
for (let i = 0; i < transactionLinks.length; i++) {
|
||||
await transactionLinkFactory(seedClient, transactionLinks[i])
|
||||
for (const transactionLink of transactionLinks) {
|
||||
await transactionLinkFactory(seedClient, transactionLink)
|
||||
}
|
||||
logger.info('##seed## seeding all transactionLinks successful...')
|
||||
|
||||
// create Contribution Links
|
||||
for (let i = 0; i < contributionLinks.length; i++) {
|
||||
await contributionLinkFactory(seedClient, contributionLinks[i])
|
||||
for (const contributionLink of contributionLinks) {
|
||||
await contributionLinkFactory(seedClient, contributionLink)
|
||||
}
|
||||
logger.info('##seed## seeding all contributionLinks successful...')
|
||||
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* 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 express, { Express, json, urlencoded } from 'express'
|
||||
import { Logger } from 'log4js'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { schema } from '@/graphql/schema'
|
||||
import { connection } from '@/typeorm/connection'
|
||||
import { Connection } from '@/typeorm/connection'
|
||||
import { checkDBVersion } from '@/typeorm/DBVersion'
|
||||
import { elopageWebhook } from '@/webhook/elopage'
|
||||
|
||||
@ -21,7 +21,11 @@ import { plugins } from './plugins'
|
||||
// TODO implement
|
||||
// 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 (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -33,8 +37,8 @@ export const createServer = async (
|
||||
logger.debug('createServer...')
|
||||
|
||||
// open mysql connection
|
||||
const con = await connection()
|
||||
if (!con || !con.isConnected) {
|
||||
const con = await Connection.getInstance()
|
||||
if (!con?.isConnected) {
|
||||
logger.fatal(`Couldn't open connection to database!`)
|
||||
throw new Error(`Fatal: Couldn't open connection to database`)
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import { configure, getLogger } from 'log4js'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
|
||||
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
||||
const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8'))
|
||||
|
||||
options.categories.backend.level = CONFIG.LOG_LEVEL
|
||||
|
||||
@ -12,7 +12,7 @@ const setHeadersPlugin = {
|
||||
return {
|
||||
willSendResponse(requestContext: any) {
|
||||
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)) {
|
||||
requestContext.response.http.headers.set(key, value)
|
||||
} else {
|
||||
@ -27,8 +27,8 @@ const setHeadersPlugin = {
|
||||
|
||||
const filterVariables = (variables: any) => {
|
||||
const vars = clonedeep(variables)
|
||||
if (vars && vars.password) vars.password = '***'
|
||||
if (vars && vars.passwordNew) vars.passwordNew = '***'
|
||||
if (vars?.password) vars.password = '***'
|
||||
if (vars?.passwordNew) vars.passwordNew = '***'
|
||||
return vars
|
||||
}
|
||||
|
||||
|
||||
@ -14,10 +14,10 @@ const getDBVersion = async (): Promise<string | null> => {
|
||||
|
||||
const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => {
|
||||
const dbVersion = await getDBVersion()
|
||||
if (!dbVersion || dbVersion.indexOf(DB_VERSION) === -1) {
|
||||
if (!dbVersion?.includes(DB_VERSION)) {
|
||||
logger.error(
|
||||
`Wrong database version detected - the backend requires '${DB_VERSION}' but found '${
|
||||
dbVersion || 'None'
|
||||
dbVersion ?? 'None'
|
||||
}`,
|
||||
)
|
||||
return false
|
||||
|
||||
@ -1,33 +1,55 @@
|
||||
// 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
|
||||
import { Connection, createConnection, FileLogger } from '@dbTools/typeorm'
|
||||
import { Connection as DbConnection, createConnection, FileLogger } from '@dbTools/typeorm'
|
||||
import { entities } from '@entity/index'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
|
||||
export const connection = async (): Promise<Connection | null> => {
|
||||
try {
|
||||
return createConnection({
|
||||
name: 'default',
|
||||
type: 'mysql',
|
||||
host: CONFIG.DB_HOST,
|
||||
port: CONFIG.DB_PORT,
|
||||
username: CONFIG.DB_USER,
|
||||
password: CONFIG.DB_PASSWORD,
|
||||
database: CONFIG.DB_DATABASE,
|
||||
entities,
|
||||
synchronize: false,
|
||||
logging: true,
|
||||
logger: new FileLogger('all', {
|
||||
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
|
||||
}),
|
||||
extra: {
|
||||
charset: 'utf8mb4_unicode_ci',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error)
|
||||
return 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 {
|
||||
Connection.instance = await createConnection({
|
||||
name: 'default',
|
||||
type: 'mysql',
|
||||
host: CONFIG.DB_HOST,
|
||||
port: CONFIG.DB_PORT,
|
||||
username: CONFIG.DB_USER,
|
||||
password: CONFIG.DB_PASSWORD,
|
||||
database: CONFIG.DB_DATABASE,
|
||||
entities,
|
||||
synchronize: false,
|
||||
logging: true,
|
||||
logger: new FileLogger('all', {
|
||||
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
|
||||
}),
|
||||
extra: {
|
||||
charset: 'utf8mb4_unicode_ci',
|
||||
},
|
||||
})
|
||||
return Connection.instance
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
backend/src/util/executeKlicktipp.ts
Normal file
16
backend/src/util/executeKlicktipp.ts
Normal 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()
|
||||
65
backend/src/util/klicktipp.test.ts
Normal file
65
backend/src/util/klicktipp.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,18 +1,14 @@
|
||||
// eslint-disable @typescript-eslint/no-explicit-any
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { getKlickTippUser } from '@/apis/KlicktippController'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { connection } from '@/typeorm/connection'
|
||||
import { getKlickTippUser, addFieldsToSubscriber } from '@/apis/KlicktippController'
|
||||
import { EventType } from '@/event/EventType'
|
||||
import { lastDateTimeEvents } from '@/graphql/resolver/util/eventList'
|
||||
|
||||
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 notRegisteredUser = []
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
const user = users[i]
|
||||
for (const user of users) {
|
||||
try {
|
||||
await getKlickTippUser(user.emailContact.email)
|
||||
} catch (err) {
|
||||
@ -21,10 +17,39 @@ export async function retrieveNotRegisteredEmails(): Promise<string[]> {
|
||||
console.log(`${user.emailContact.email}`)
|
||||
}
|
||||
}
|
||||
await con.close()
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('User die nicht bei KlickTipp vorhanden sind: ', 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
|
||||
}
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import i18n from 'i18n'
|
||||
|
||||
export const objectValuesToArray = (obj: { [x: string]: string }): Array<string> => {
|
||||
return Object.keys(obj).map(function (key) {
|
||||
return obj[key]
|
||||
})
|
||||
}
|
||||
export const objectValuesToArray = (obj: Record<string, string>): string[] =>
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
Object.keys(obj).map((key) => obj[key])
|
||||
|
||||
export const decimalSeparatorByLanguage = (a: Decimal, language: string): string => {
|
||||
const rememberLocaleToRestore = i18n.getLocale()
|
||||
@ -14,3 +12,6 @@ export const decimalSeparatorByLanguage = (a: Decimal, language: string): string
|
||||
i18n.setLocale(rememberLocaleToRestore)
|
||||
return result
|
||||
}
|
||||
|
||||
export const fullName = (firstName: string, lastName: string): string =>
|
||||
[firstName, lastName].filter(Boolean).join(' ')
|
||||
|
||||
@ -54,6 +54,10 @@ const virtualLinkTransaction = (
|
||||
creationDate: null,
|
||||
contribution: null,
|
||||
...defaultModelFunctions,
|
||||
userGradidoID: '',
|
||||
userName: null,
|
||||
linkedUserGradidoID: null,
|
||||
linkedUserName: null,
|
||||
}
|
||||
return new Transaction(linkDbTransaction, user)
|
||||
}
|
||||
@ -84,6 +88,10 @@ const virtualDecayTransaction = (
|
||||
creationDate: null,
|
||||
contribution: null,
|
||||
...defaultModelFunctions,
|
||||
userGradidoID: '',
|
||||
userName: null,
|
||||
linkedUserGradidoID: null,
|
||||
linkedUserName: null,
|
||||
}
|
||||
return new Transaction(decayDbTransaction, user)
|
||||
}
|
||||
|
||||
@ -115,6 +115,7 @@ export const elopageWebhook = async (req: any, res: any): Promise<void> => {
|
||||
) {
|
||||
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_NAME = /^<>&;]{2,}$/
|
||||
|
||||
@ -146,7 +147,7 @@ export const elopageWebhook = async (req: any, res: any): Promise<void> => {
|
||||
email,
|
||||
firstName,
|
||||
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) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
@ -22,8 +22,8 @@ const context = {
|
||||
|
||||
export const cleanDB = async () => {
|
||||
// this only works as lond we do not have foreign key constraints
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
await resetEntity(entities[i])
|
||||
for (const entity of entities) {
|
||||
await resetEntity(entity)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -382,6 +382,14 @@
|
||||
dependencies:
|
||||
"@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":
|
||||
version "4.2.0"
|
||||
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"
|
||||
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@*":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72"
|
||||
@ -1994,11 +1995,6 @@ bser@2.1.1:
|
||||
dependencies:
|
||||
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:
|
||||
version "1.1.2"
|
||||
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"
|
||||
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:
|
||||
version "1.1.1"
|
||||
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"
|
||||
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:
|
||||
version "1.0.0"
|
||||
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==
|
||||
|
||||
"gradido-database@file:../database":
|
||||
version "1.19.1"
|
||||
version "1.20.0"
|
||||
dependencies:
|
||||
"@types/uuid" "^8.3.4"
|
||||
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"
|
||||
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
|
||||
|
||||
ignore@^5.2.0:
|
||||
ignore@^5.2.0, ignore@^5.2.4:
|
||||
version "5.2.4"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
|
||||
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
|
||||
@ -4790,6 +4786,11 @@ jest@^27.2.4:
|
||||
import-local "^3.0.2"
|
||||
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:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711"
|
||||
@ -4903,22 +4904,6 @@ jsonfile@^6.0.1:
|
||||
optionalDependencies:
|
||||
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:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3"
|
||||
@ -4938,23 +4923,6 @@ juice@^8.0.0:
|
||||
slick "^1.12.2"
|
||||
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:
|
||||
version "3.1.0"
|
||||
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"
|
||||
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:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||
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:
|
||||
version "4.7.0"
|
||||
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"
|
||||
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:
|
||||
version "1.4.3"
|
||||
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"
|
||||
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":
|
||||
version "2.1.2"
|
||||
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:
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export { Transaction } from './0036-unique_previous_in_transactions/Transaction'
|
||||
export { Transaction } from './0066-x-community-sendcoins-transactions_table/Transaction'
|
||||
|
||||
@ -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`;')
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-database",
|
||||
"version": "1.20.0",
|
||||
"version": "1.21.0",
|
||||
"description": "Gradido Database Tool to execute database migrations",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/database",
|
||||
|
||||
@ -57,7 +57,7 @@ EMAIL_CODE_REQUEST_TIME=10
|
||||
WEBHOOK_ELOPAGE_SECRET=secret
|
||||
|
||||
# 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
|
||||
# on an hash created from this topic
|
||||
# FEDERATION_DHT_TOPIC=GRADIDO_HUB
|
||||
|
||||
@ -117,7 +117,7 @@ server {
|
||||
|
||||
# TODO this could be a performance optimization
|
||||
#location /vue {
|
||||
# alias /var/www/html/gradido/frontend/dist;
|
||||
# alias /var/www/html/gradido/frontend/build;
|
||||
# index index.html;
|
||||
#
|
||||
# location ~* \.(png)$ {
|
||||
|
||||
@ -103,7 +103,7 @@ server {
|
||||
|
||||
# TODO this could be a performance optimization
|
||||
#location /vue {
|
||||
# alias /var/www/html/gradido/frontend/dist;
|
||||
# alias /var/www/html/gradido/frontend/build;
|
||||
# index index.html;
|
||||
#
|
||||
# location ~* \.(png)$ {
|
||||
|
||||
@ -15,6 +15,6 @@ export NVM_DIR="/root/.nvm"
|
||||
$NPM_BIN install
|
||||
$NPM_BIN run build
|
||||
# prezip for faster deliver throw nginx
|
||||
cd dist
|
||||
cd build
|
||||
find . -type f -name "*.css" -exec gzip -9 -k {} \;
|
||||
find . -type f -name "*.js" -exec gzip -9 -k {} \;
|
||||
|
||||
@ -130,6 +130,15 @@ rm -Rf $PROJECT_ROOT/admin/node_modules
|
||||
rm -Rf $PROJECT_ROOT/dht-node/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
|
||||
cp -f $PROJECT_ROOT/database/.env $PROJECT_ROOT/database/.env.bak
|
||||
cp -f $PROJECT_ROOT/backend/.env $PROJECT_ROOT/backend/.env.bak
|
||||
|
||||
@ -8,6 +8,10 @@ DB_PASSWORD=$DB_PASSWORD
|
||||
DB_DATABASE=gradido_community
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=$TYPEORM_LOGGING_RELATIVE_PATH
|
||||
|
||||
# Community
|
||||
COMMUNITY_NAME=$COMMUNITY_NAME
|
||||
COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION
|
||||
|
||||
# Federation
|
||||
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
|
||||
|
||||
@ -6,7 +6,7 @@ module.exports = {
|
||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 80,
|
||||
lines: 83,
|
||||
},
|
||||
},
|
||||
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-dht-node",
|
||||
"version": "1.20.0",
|
||||
"version": "1.21.0",
|
||||
"description": "Gradido dht-node module",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/",
|
||||
@ -23,7 +23,8 @@
|
||||
"nodemon": "^2.0.20",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.1.2",
|
||||
"typescript": "^4.9.4"
|
||||
"typescript": "^4.9.4",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dotenv": "^8.2.0",
|
||||
@ -31,6 +32,7 @@
|
||||
"@types/node": "^18.11.18",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.0",
|
||||
"@typescript-eslint/parser": "^5.48.0",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"eslint": "^8.31.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
|
||||
@ -3,13 +3,13 @@ import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0065-refactor_communities_table',
|
||||
DB_VERSION: '0066-x-community-sendcoins-transactions_table',
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v2.2023-02-07',
|
||||
EXPECTED: 'v3.2023-04-26',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
@ -28,6 +28,12 @@ const database = {
|
||||
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 = {
|
||||
FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || 'GRADIDO_HUB',
|
||||
FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
|
||||
@ -51,6 +57,7 @@ const CONFIG = {
|
||||
...constants,
|
||||
...server,
|
||||
...database,
|
||||
...community,
|
||||
...federation,
|
||||
}
|
||||
|
||||
|
||||
@ -5,8 +5,10 @@ import { startDHT } from './index'
|
||||
import DHT from '@hyperswarm/dht'
|
||||
import CONFIG from '@/config'
|
||||
import { logger } from '@test/testSetup'
|
||||
import { Community as DbCommunity } from '@entity/Community'
|
||||
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||
import { testEnvironment, cleanDB } from '@test/helpers'
|
||||
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
||||
|
||||
CONFIG.FEDERATION_DHT_SEED = '64ebcb0e3ad547848fef4197c6e2332f'
|
||||
|
||||
@ -114,6 +116,9 @@ describe('federation', () => {
|
||||
const hashSpy = jest.spyOn(DHT, 'hash')
|
||||
const keyPairSpy = jest.spyOn(DHT, 'keyPair')
|
||||
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()
|
||||
jest.clearAllMocks()
|
||||
await cleanDB()
|
||||
@ -132,6 +137,64 @@ describe('federation', () => {
|
||||
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', () => {
|
||||
it('creates a server', () => {
|
||||
expect(nodeCreateServerMock).toBeCalled()
|
||||
@ -780,21 +843,21 @@ describe('federation', () => {
|
||||
socketEventMocks.open()
|
||||
})
|
||||
|
||||
it.skip('calls socket write with own api versions', () => {
|
||||
it('calls socket write with own api versions', () => {
|
||||
expect(socketWriteMock).toBeCalledWith(
|
||||
Buffer.from(
|
||||
JSON.stringify([
|
||||
{
|
||||
api: '1_0',
|
||||
url: 'http://localhost/api/',
|
||||
url: 'https://test.gradido.net/api/',
|
||||
},
|
||||
{
|
||||
api: '1_1',
|
||||
url: 'http://localhost/api/',
|
||||
url: 'https://test.gradido.net/api/',
|
||||
},
|
||||
{
|
||||
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/',
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -4,10 +4,15 @@ import DHT from '@hyperswarm/dht'
|
||||
import { logger } from '@/server/logger'
|
||||
import CONFIG from '@/config'
|
||||
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 getSeed = (): Buffer | null =>
|
||||
CONFIG.FEDERATION_DHT_SEED ? Buffer.alloc(KEY_SECRET_SEEDBYTES, CONFIG.FEDERATION_DHT_SEED) : null
|
||||
const getSeed = (): Buffer | null => {
|
||||
return CONFIG.FEDERATION_DHT_SEED
|
||||
? Buffer.alloc(KEY_SECRET_SEEDBYTES, CONFIG.FEDERATION_DHT_SEED)
|
||||
: null
|
||||
}
|
||||
|
||||
const POLLTIME = 20000
|
||||
const SUCCESSTIME = 120000
|
||||
@ -28,10 +33,12 @@ export const startDHT = async (topic: string): Promise<void> => {
|
||||
try {
|
||||
const TOPIC = DHT.hash(Buffer.from(topic))
|
||||
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')}`)
|
||||
await writeHomeCommunityEntry(pubKeyString)
|
||||
|
||||
const ownApiVersions = await writeFederatedHomeCommunityEnries(keyPair.publicKey)
|
||||
const ownApiVersions = await writeFederatedHomeCommunityEntries(pubKeyString)
|
||||
logger.info(`ApiList: ${JSON.stringify(ownApiVersions)}`)
|
||||
|
||||
const node = new DHT({ keyPair })
|
||||
@ -138,7 +145,7 @@ export const startDHT = async (topic: string): Promise<void> => {
|
||||
data.peers.forEach((peer: any) => {
|
||||
const pubKey = peer.publicKey.toString('hex')
|
||||
if (
|
||||
pubKey !== keyPair.publicKey.toString('hex') &&
|
||||
pubKey !== pubKeyString &&
|
||||
!successfulRequests.includes(pubKey) &&
|
||||
!errorfulRequests.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 comApi: CommunityApi = {
|
||||
api: apiEnum,
|
||||
@ -189,21 +196,65 @@ async function writeFederatedHomeCommunityEnries(pubKey: any): Promise<Community
|
||||
})
|
||||
try {
|
||||
// first remove privious existing homeCommunity entries
|
||||
DbFederatedCommunity.createQueryBuilder().delete().where({ foreign: false }).execute()
|
||||
|
||||
homeApiVersions.forEach(async function (homeApi) {
|
||||
const homeCom = new DbFederatedCommunity()
|
||||
await DbFederatedCommunity.createQueryBuilder().delete().where({ foreign: false }).execute()
|
||||
for (const homeApiVersion of homeApiVersions) {
|
||||
const homeCom = DbFederatedCommunity.create()
|
||||
homeCom.foreign = false
|
||||
homeCom.apiVersion = homeApi.api
|
||||
homeCom.endPoint = homeApi.url
|
||||
homeCom.publicKey = pubKey.toString('hex')
|
||||
|
||||
// this will NOT update the updatedAt column, to distingue between a normal update and the last announcement
|
||||
homeCom.apiVersion = homeApiVersion.api
|
||||
homeCom.endPoint = homeApiVersion.url
|
||||
homeCom.publicKey = Buffer.from(pubKey)
|
||||
await DbFederatedCommunity.insert(homeCom)
|
||||
logger.info(`federation home-community inserted successfully: ${JSON.stringify(homeCom)}`)
|
||||
})
|
||||
logger.info(`federation home-community inserted successfully:`, homeApiVersion)
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`Federation: Error writing HomeCommunity-Entries: ${err}`)
|
||||
throw new Error(`Federation: Error writing federated HomeCommunity-Entries: ${err}`)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,9 +21,8 @@ async function main() {
|
||||
logger.fatal('Fatal: Database Version incorrect')
|
||||
throw new Error('Fatal: Database Version incorrect')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
logger.debug(`dhtseed set by CONFIG.FEDERATION_DHT_SEED=${CONFIG.FEDERATION_DHT_SEED}`)
|
||||
logger.info(
|
||||
`starting Federation on ${CONFIG.FEDERATION_DHT_TOPIC} ${
|
||||
CONFIG.FEDERATION_DHT_SEED ? 'with seed...' : 'without seed...'
|
||||
}`,
|
||||
|
||||
@ -22,8 +22,8 @@ const context = {
|
||||
|
||||
export const cleanDB = async () => {
|
||||
// this only works as long we do not have foreign key constraints
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
await resetEntity(entities[i])
|
||||
for (const entity of entities) {
|
||||
await resetEntity(entity)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -769,6 +769,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
|
||||
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@*":
|
||||
version "21.0.0"
|
||||
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"
|
||||
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:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
|
||||
|
||||
@ -58,7 +58,7 @@ export default defineConfig({
|
||||
mailserverURL: 'http://localhost:1080',
|
||||
loginQuery: `mutation ($email: String!, $password: String!, $publisherId: Int) {
|
||||
login(email: $email, password: $password, publisherId: $publisherId) {
|
||||
email
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
language
|
||||
|
||||
@ -35,6 +35,6 @@ Cypress.Commands.add('login', (email, password) => {
|
||||
}
|
||||
|
||||
cy.visit('/')
|
||||
window.localStorage.setItem('vuex', JSON.stringify(vuexToken))
|
||||
window.localStorage.setItem('gradido-frontend', JSON.stringify(vuexToken))
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"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",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/federation",
|
||||
|
||||
@ -11,7 +11,7 @@ Decimal.set({
|
||||
*/
|
||||
|
||||
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
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
|
||||
@ -23,8 +23,8 @@ const setHeadersPlugin = {
|
||||
|
||||
const filterVariables = (variables: any) => {
|
||||
const vars = clonedeep(variables)
|
||||
if (vars.password) vars.password = '***'
|
||||
if (vars.passwordNew) vars.passwordNew = '***'
|
||||
if (vars && vars.password) vars.password = '***'
|
||||
if (vars && vars.passwordNew) vars.passwordNew = '***'
|
||||
return vars
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
coverage/
|
||||
2
frontend/.gitignore
vendored
2
frontend/.gitignore
vendored
@ -1,6 +1,6 @@
|
||||
.DS_Store
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
.cache/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
||||
@ -84,7 +84,7 @@ CMD /bin/sh -c "yarn run dev"
|
||||
FROM base as production
|
||||
|
||||
# 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
|
||||
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
|
||||
# Copy static files
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "bootstrap-vue-gradido-wallet",
|
||||
"version": "1.20.0",
|
||||
"version": "1.21.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node run/server.js",
|
||||
"serve": "vue-cli-service serve --open",
|
||||
"build": "vue-cli-service build",
|
||||
"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 .",
|
||||
"stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'",
|
||||
"test": "cross-env TZ=UTC jest",
|
||||
@ -50,6 +50,7 @@
|
||||
"prettier": "^2.2.1",
|
||||
"qrcanvas-vue": "2.1.1",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"uuid": "^9.0.0",
|
||||
"vee-validate": "^3.4.5",
|
||||
"vue": "2.6.12",
|
||||
"vue-apollo": "^3.0.7",
|
||||
|
||||
@ -9,10 +9,10 @@ const port = process.env.PORT || 3000
|
||||
// Express Server
|
||||
const app = express()
|
||||
// Serve files
|
||||
app.use(express.static(path.join(__dirname, '../dist')))
|
||||
app.use(express.static(path.join(__dirname, '../build')))
|
||||
// Default to index.html
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../dist/index.html'))
|
||||
res.sendFile(path.join(__dirname, '../build/index.html'))
|
||||
})
|
||||
|
||||
app.listen(port, hostname, () => {
|
||||
|
||||
@ -3,15 +3,23 @@
|
||||
<div class="bg-white appBoxShadow gradido-border-radius p-3">
|
||||
<div class="h3 mb-4">{{ $t('form.send_check') }}</div>
|
||||
<b-row class="mt-5">
|
||||
<b-col cols="2"></b-col>
|
||||
<b-col>
|
||||
<div class="h4">{{ userName ? userName : identifier }}</div>
|
||||
<div class="mt-3 h5">{{ $t('form.memo') }}</div>
|
||||
<div>{{ memo }}</div>
|
||||
</b-col>
|
||||
<b-col cols="3">
|
||||
<div class="small">{{ $t('send_gdd') }}</div>
|
||||
<div>{{ amount | GDD }}</div>
|
||||
<b-col cols="12">
|
||||
<b-row class="mt-3">
|
||||
<b-col class="h5">{{ $t('form.recipientCommunity') }}</b-col>
|
||||
<b-col>{{ communityName }}</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col class="h5">{{ $t('form.recipient') }}</b-col>
|
||||
<b-col>{{ userName ? userName : identifier }}</b-col>
|
||||
</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-row>
|
||||
|
||||
@ -58,6 +66,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { COMMUNITY_NAME } from '@/config'
|
||||
|
||||
export default {
|
||||
name: 'TransactionConfirmationSend',
|
||||
props: {
|
||||
@ -70,6 +80,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
disabled: false,
|
||||
communityName: COMMUNITY_NAME,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@ -71,9 +71,9 @@ describe('TransactionForm', () => {
|
||||
})
|
||||
|
||||
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(
|
||||
wrapper.find('div[data-test="input-email"]').find('input').attributes('disabled'),
|
||||
wrapper.find('div[data-test="input-identifier"]').find('input').attributes('disabled'),
|
||||
).toBe('disabled')
|
||||
})
|
||||
|
||||
@ -116,51 +116,54 @@ describe('TransactionForm', () => {
|
||||
expect(wrapper.vm.radioSelected).toBe(SEND_TYPES.send)
|
||||
})
|
||||
|
||||
describe('email field', () => {
|
||||
it('has an input field of type email', () => {
|
||||
describe('identifier field', () => {
|
||||
it('has an input field of type text', () => {
|
||||
expect(
|
||||
wrapper.find('div[data-test="input-email"]').find('input').attributes('type'),
|
||||
).toBe('email')
|
||||
wrapper.find('div[data-test="input-identifier"]').find('input').attributes('type'),
|
||||
).toBe('text')
|
||||
})
|
||||
|
||||
it('has a label form.receiver', () => {
|
||||
expect(wrapper.find('div[data-test="input-email"]').find('label').text()).toBe(
|
||||
it('has a label form.recipient', () => {
|
||||
expect(wrapper.find('div[data-test="input-identifier"]').find('label').text()).toBe(
|
||||
'form.recipient',
|
||||
)
|
||||
})
|
||||
|
||||
it('has a placeholder "E-Mail"', () => {
|
||||
it('has a placeholder for identifier', () => {
|
||||
expect(
|
||||
wrapper.find('div[data-test="input-email"]').find('input').attributes('placeholder'),
|
||||
).toBe('form.email')
|
||||
wrapper
|
||||
.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 () => {
|
||||
await wrapper.find('div[data-test="input-email"]').find('input').setValue('a')
|
||||
it('flushes an error message when no valid identifier is given', async () => {
|
||||
await wrapper.find('div[data-test="input-identifier"]').find('input').setValue('a')
|
||||
await flushPromises()
|
||||
expect(
|
||||
wrapper.find('div[data-test="input-email"]').find('.invalid-feedback').text(),
|
||||
).toBe('validations.messages.email')
|
||||
wrapper.find('div[data-test="input-identifier"]').find('.invalid-feedback').text(),
|
||||
).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.
|
||||
it.skip('flushes an error message when email is the email of logged in user', async () => {
|
||||
await wrapper
|
||||
.find('div[data-test="input-email"]')
|
||||
.find('div[data-test="input-identifier"]')
|
||||
.find('input')
|
||||
.setValue('user@example.org')
|
||||
await flushPromises()
|
||||
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')
|
||||
})
|
||||
|
||||
it('trims the email after blur', async () => {
|
||||
it('trims the identifier after blur', async () => {
|
||||
await wrapper
|
||||
.find('div[data-test="input-email"]')
|
||||
.find('div[data-test="input-identifier"]')
|
||||
.find('input')
|
||||
.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()
|
||||
expect(wrapper.vm.form.identifier).toBe('valid@email.com')
|
||||
})
|
||||
@ -304,7 +307,7 @@ Die ganze Welt bezwingen.“`)
|
||||
|
||||
it('clears all fields on click', async () => {
|
||||
await wrapper
|
||||
.find('div[data-test="input-email"]')
|
||||
.find('div[data-test="input-identifier"]')
|
||||
.find('input')
|
||||
.setValue('someone@watches.tv')
|
||||
await wrapper.find('div[data-test="input-amount"]').find('input').setValue('87.23')
|
||||
@ -327,7 +330,7 @@ Die ganze Welt bezwingen.“`)
|
||||
describe('submit', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper
|
||||
.find('div[data-test="input-email"]')
|
||||
.find('div[data-test="input-identifier"]')
|
||||
.find('input')
|
||||
.setValue('someone@watches.tv')
|
||||
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', () => {
|
||||
it('has no email input field', () => {
|
||||
expect(wrapper.find('div[data-test="input-email"]').exists()).toBe(false)
|
||||
it('has no identifier input field', () => {
|
||||
expect(wrapper.find('div[data-test="input-identifier"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('queries the username', () => {
|
||||
|
||||
@ -49,12 +49,20 @@
|
||||
<b-row>
|
||||
<b-col>
|
||||
<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">
|
||||
<div v-if="!gradidoID">
|
||||
<input-email
|
||||
<input-identifier
|
||||
:name="$t('form.recipient')"
|
||||
:label="$t('form.recipient')"
|
||||
:placeholder="$t('form.email')"
|
||||
:placeholder="$t('form.identifier')"
|
||||
v-model="form.identifier"
|
||||
:disabled="isBalanceDisabled"
|
||||
@onValidation="onValidation"
|
||||
@ -126,16 +134,17 @@
|
||||
</template>
|
||||
<script>
|
||||
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 InputTextarea from '@/components/Inputs/InputTextarea'
|
||||
import { user as userQuery } from '@/graphql/queries'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { COMMUNITY_NAME } from '@/config'
|
||||
|
||||
export default {
|
||||
name: 'TransactionForm',
|
||||
components: {
|
||||
InputEmail,
|
||||
InputIdentifier,
|
||||
InputAmount,
|
||||
InputTextarea,
|
||||
},
|
||||
@ -155,6 +164,7 @@ export default {
|
||||
},
|
||||
radioSelected: this.selected,
|
||||
userName: '',
|
||||
communityName: COMMUNITY_NAME,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
68
frontend/src/components/Inputs/InputIdentifier.vue
Normal file
68
frontend/src/components/Inputs/InputIdentifier.vue
Normal 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>
|
||||
@ -8,7 +8,7 @@
|
||||
containsLowercaseCharacter: true,
|
||||
containsUppercaseCharacter: true,
|
||||
containsNumericCharacter: true,
|
||||
atLeastEightCharactera: true,
|
||||
atLeastEightCharacters: true,
|
||||
atLeastOneSpecialCharater: true,
|
||||
noWhitespaceCharacters: true,
|
||||
}"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user