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

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

View File

@ -4,8 +4,49 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [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)

View File

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

2
admin/.gitignore vendored
View File

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

View File

@ -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

View File

@ -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",

View File

@ -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, () => {

View File

@ -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 }

View File

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

View File

@ -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'),
}

View File

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

View File

@ -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,
},
},

View File

@ -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'],

View File

@ -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",

View File

@ -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 {

View File

@ -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
}

View File

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

View File

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

View File

@ -1,22 +1,33 @@
import { verify, sign } from 'jsonwebtoken'
import { SignJWT, jwtVerify } from 'jose'
import { CONFIG } from '@/config/'
import { 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
}

View File

@ -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',

View File

@ -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,

View File

@ -1,44 +1,49 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { 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)
}
}

View File

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

View File

@ -1,44 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @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 {}

View File

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

View File

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

View File

@ -8,6 +8,8 @@
import { Connection } from '@dbTools/typeorm'
import { 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',
)
})
})

View File

@ -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
}
}

View File

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

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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()

View File

@ -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

View File

@ -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)
}
}

View File

@ -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))
})

View File

@ -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

View File

@ -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,
})
})
})
})
})

View File

@ -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
}
}

View File

@ -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,
})
})
})
})
})

View File

@ -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')

View File

@ -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],
}
})

View File

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

View File

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

View File

@ -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)
}

View File

@ -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,

View File

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

View File

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

View File

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

View File

@ -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 },

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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...')

View File

@ -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`)
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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
}
}
}

View File

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

View File

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

View File

@ -1,18 +1,14 @@
// eslint-disable @typescript-eslint/no-explicit-any
import { User } from '@entity/User'
import { 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
}

View File

@ -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(' ')

View File

@ -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)
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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==

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "gradido-database",
"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",

View File

@ -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

View File

@ -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)$ {

View File

@ -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)$ {

View File

@ -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 {} \;

View File

@ -130,6 +130,15 @@ rm -Rf $PROJECT_ROOT/admin/node_modules
rm -Rf $PROJECT_ROOT/dht-node/node_modules
rm -Rf $PROJECT_ROOT/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

View File

@ -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

View File

@ -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'],

View File

@ -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",

View File

@ -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,
}

View File

@ -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/',
},
])
})
})
})
})
})

View File

@ -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)
}
}

View File

@ -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...'
}`,

View File

@ -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)
}
}

View File

@ -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"

View File

@ -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

View File

@ -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))
})
})

View File

@ -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",

View File

@ -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

View File

@ -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
}

View File

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

2
frontend/.gitignore vendored
View File

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

View File

@ -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

View File

@ -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",

View File

@ -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, () => {

View File

@ -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,
}
},
}

View File

@ -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', () => {

View File

@ -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: {

View File

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

View File

@ -8,7 +8,7 @@
containsLowercaseCharacter: true,
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