Merge branch 'master' into 2897-incorrect-errormessage-for-wrong-contribution-link

This commit is contained in:
Hannes Heine 2023-05-19 10:34:02 +02:00 committed by GitHub
commit 204a2d2471
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
145 changed files with 2240 additions and 869 deletions

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

@ -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': {
@ -151,6 +153,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 +166,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,6 +177,8 @@ 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,

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

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

@ -12,12 +12,13 @@ import KlicktippConnector from 'klicktipp-api'
const klicktippConnector = new KlicktippConnector()
export const klicktippSignIn = async (
export const subscribe = async (
email: string,
language: string,
firstName?: string,
lastName?: string,
): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
const fields = {
fieldFirstName: firstName,
fieldLastName: lastName,
@ -27,13 +28,8 @@ export const klicktippSignIn = async (
return result
}
export const signout = async (email: string, language: string): Promise<boolean> => {
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()
if (isLogin) {
return await klicktippConnector.unsubscribe(email)
@ -42,6 +38,7 @@ export const unsubscribe = async (email: string): Promise<boolean> => {
}
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)
@ -52,33 +49,21 @@ export const getKlickTippUser = async (email: string): Promise<any> => {
}
export const loginKlicktippUser = async (): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
return await klicktippConnector.login(CONFIG.KLICKTIPP_USER, CONFIG.KLICKTIPP_PASSWORD)
}
export const logoutKlicktippUser = async (): Promise<boolean> => {
return await klicktippConnector.logout()
}
export const untagUser = async (email: string, tagId: string): Promise<boolean> => {
export const addFieldsToSubscriber = async (
email: string,
fields: any = {},
newemail = '',
newsmsnumber = '',
) => {
if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser()
if (isLogin) {
return await klicktippConnector.untag(email, tagId)
const subscriberId = await klicktippConnector.subscriberSearch(email)
return klicktippConnector.subscriberUpdate(subscriberId, fields, newemail, newsmsnumber)
}
return false
}
export const tagUser = async (email: string, tagIds: string): Promise<boolean> => {
const isLogin = await loginKlicktippUser()
if (isLogin) {
return await klicktippConnector.tag(email, tagIds)
}
return false
}
export const getKlicktippTagMap = async () => {
const isLogin = await loginKlicktippUser()
if (isLogin) {
return await klicktippConnector.tagIndex()
}
return ''
}

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

@ -5,8 +5,6 @@ export enum RIGHTS {
COMMUNITIES = 'COMMUNITIES',
LIST_GDT_ENTRIES = 'LIST_GDT_ENTRIES',
EXIST_PID = 'EXIST_PID',
GET_KLICKTIPP_USER = 'GET_KLICKTIPP_USER',
GET_KLICKTIPP_TAG_MAP = 'GET_KLICKTIPP_TAG_MAP',
UNSUBSCRIBE_NEWSLETTER = 'UNSUBSCRIBE_NEWSLETTER',
SUBSCRIBE_NEWSLETTER = 'SUBSCRIBE_NEWSLETTER',
TRANSACTION_LIST = 'TRANSACTION_LIST',
@ -36,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

@ -9,8 +9,6 @@ export const ROLE_USER = new Role('user', [
RIGHTS.BALANCE,
RIGHTS.LIST_GDT_ENTRIES,
RIGHTS.EXIST_PID,
RIGHTS.GET_KLICKTIPP_USER,
RIGHTS.GET_KLICKTIPP_TAG_MAP,
RIGHTS.UNSUBSCRIBE_NEWSLETTER,
RIGHTS.SUBSCRIBE_NEWSLETTER,
RIGHTS.TRANSACTION_LIST,

View File

@ -12,11 +12,11 @@ Decimal.set({
})
const constants = {
DB_VERSION: '0064-event_rename',
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

@ -0,0 +1,8 @@
import { Event as DbEvent } from '@entity/Event'
import { User as DbUser } from '@entity/User'
import { Event } from './Event'
import { EventType } from './EventType'
export const EVENT_NEWSLETTER_SUBSCRIBE = async (user: DbUser): Promise<DbEvent> =>
Event(EventType.NEWSLETTER_SUBSCRIBE, user, user).save()

View File

@ -0,0 +1,8 @@
import { Event as DbEvent } from '@entity/Event'
import { User as DbUser } from '@entity/User'
import { Event } from './Event'
import { EventType } from './EventType'
export const EVENT_NEWSLETTER_UNSUBSCRIBE = async (user: DbUser): Promise<DbEvent> =>
Event(EventType.NEWSLETTER_UNSUBSCRIBE, user, user).save()

View File

@ -21,6 +21,8 @@ export enum EventType {
EMAIL_ADMIN_CONFIRMATION = 'EMAIL_ADMIN_CONFIRMATION',
EMAIL_CONFIRMATION = 'EMAIL_CONFIRMATION',
EMAIL_FORGOT_PASSWORD = 'EMAIL_FORGOT_PASSWORD',
NEWSLETTER_SUBSCRIBE = 'NEWSLETTER_SUBSCRIBE',
NEWSLETTER_UNSUBSCRIBE = 'NEWSLETTER_UNSUBSCRIBE',
TRANSACTION_SEND = 'TRANSACTION_SEND',
TRANSACTION_RECEIVE = 'TRANSACTION_RECEIVE',
TRANSACTION_LINK_CREATE = 'TRANSACTION_LINK_CREATE',

View File

@ -21,6 +21,8 @@ export { EVENT_EMAIL_ACCOUNT_MULTIREGISTRATION } from './EVENT_EMAIL_ACCOUNT_MUL
export { EVENT_EMAIL_ADMIN_CONFIRMATION } from './EVENT_EMAIL_ADMIN_CONFIRMATION'
export { EVENT_EMAIL_CONFIRMATION } from './EVENT_EMAIL_CONFIRMATION'
export { EVENT_EMAIL_FORGOT_PASSWORD } from './EVENT_EMAIL_FORGOT_PASSWORD'
export { EVENT_NEWSLETTER_SUBSCRIBE } from './EVENT_NEWSLETTER_SUBSCRIBE'
export { EVENT_NEWSLETTER_UNSUBSCRIBE } from './EVENT_NEWSLETTER_UNSUBSCRIBE'
export { EVENT_TRANSACTION_SEND } from './EVENT_TRANSACTION_SEND'
export { EVENT_TRANSACTION_RECEIVE } from './EVENT_TRANSACTION_RECEIVE'
export { EVENT_TRANSACTION_LINK_CREATE } from './EVENT_TRANSACTION_LINK_CREATE'

View File

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

View File

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

View File

@ -0,0 +1,58 @@
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { ApiVersionType } from '@/federation/enum/apiVersionType'
// eslint-disable-next-line camelcase
import { Client_1_0 } from './Client_1_0'
// eslint-disable-next-line camelcase
import { Client_1_1 } from './Client_1_1'
// eslint-disable-next-line camelcase
type FederationClient = Client_1_0 | Client_1_1
interface ClientInstance {
id: number
// eslint-disable-next-line no-use-before-define
client: FederationClient
}
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class Client {
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, @typescript-eslint/no-empty-function
private constructor() {}
private static createFederationClient = (dbCom: DbFederatedCommunity) => {
switch (dbCom.apiVersion) {
case ApiVersionType.V1_0:
return new Client_1_0(dbCom)
case ApiVersionType.V1_1:
return new Client_1_1(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 = Client.instanceArray.find((instance) => instance.id === dbCom.id)
if (instance) {
return instance.client
}
const client = Client.createFederationClient(dbCom)
if (client) {
Client.instanceArray.push({ id: dbCom.id, client } as ClientInstance)
}
return client
}
}

View File

@ -0,0 +1,49 @@
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { GraphQLClient } from 'graphql-request'
import { getPublicKey } from '@/federation/query/getPublicKey'
import { backendLogger as logger } from '@/server/logger'
// eslint-disable-next-line camelcase
export class Client_1_0 {
dbCom: DbFederatedCommunity
endpoint: string
client: GraphQLClient
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
}
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)
}
}
}

View File

@ -0,0 +1,5 @@
// eslint-disable-next-line camelcase
import { Client_1_0 } from './Client_1_0'
// eslint-disable-next-line camelcase
export class Client_1_1 extends Client_1_0 {}

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

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

View File

@ -1,5 +1,12 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Connection } from '@dbTools/typeorm'
import { Community as DbCommunity } from '@entity/Community'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { ApolloServerTestClient } from 'apollo-server-testing'
import { testEnvironment, cleanDB } from '@test/helpers'
@ -58,9 +65,9 @@ describe('validate Communities', () => {
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbCommunity.createQueryBuilder()
await DbFederatedCommunity.createQueryBuilder()
.insert()
.into(DbCommunity)
.into(DbFederatedCommunity)
.values(variables1)
.orUpdate({
conflict_target: ['id', 'publicKey', 'apiVersion'],
@ -77,7 +84,8 @@ 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/',
)
})
})
@ -89,9 +97,9 @@ describe('validate Communities', () => {
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbCommunity.createQueryBuilder()
await DbFederatedCommunity.createQueryBuilder()
.insert()
.into(DbCommunity)
.into(DbFederatedCommunity)
.values(variables2)
.orUpdate({
conflict_target: ['id', 'publicKey', 'apiVersion'],
@ -107,17 +115,19 @@ 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/',
)
})
})
describe('with three Communities of api 1_0, 1_1 and 2_0', () => {
let dbCom: DbCommunity
let dbCom: DbFederatedCommunity
beforeEach(async () => {
const variables3 = {
publicKey: Buffer.from('11111111111111111111111111111111'),
@ -125,16 +135,16 @@ describe('validate Communities', () => {
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbCommunity.createQueryBuilder()
await DbFederatedCommunity.createQueryBuilder()
.insert()
.into(DbCommunity)
.into(DbFederatedCommunity)
.values(variables3)
.orUpdate({
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
dbCom = await DbCommunity.findOneOrFail({
dbCom = await DbFederatedCommunity.findOneOrFail({
where: { publicKey: variables3.publicKey, apiVersion: variables3.apiVersion },
})
jest.clearAllMocks()
@ -145,18 +155,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

@ -1,13 +1,11 @@
/** eslint-disable @typescript-eslint/no-unsafe-call */
/** eslint-disable @typescript-eslint/no-unsafe-assignment */
import { IsNull } from '@dbTools/typeorm'
import { Community as DbCommunity } from '@entity/Community'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { LogError } from '@/server/LogError'
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 { Client } from './client/Client'
import { ApiVersionType } from './enum/apiVersionType'
export function startValidateCommunities(timerInterval: number): void {
@ -23,64 +21,36 @@ export function startValidateCommunities(timerInterval: number): void {
}
export async function validateCommunities(): Promise<void> {
const dbCommunities: DbCommunity[] = await DbCommunity.createQueryBuilder()
.where({ foreign: true, verifiedAt: IsNull() })
.orWhere('verified_at < last_announced_at')
.getMany()
const dbFederatedCommunities: DbFederatedCommunity[] =
await DbFederatedCommunity.createQueryBuilder()
.where({ foreign: true, verifiedAt: IsNull() })
.orWhere('verified_at < last_announced_at')
.getMany()
logger.debug(`Federation: found ${dbCommunities.length} dbCommunities`)
for (const dbCom of dbCommunities) {
logger.debug(`Federation: found ${dbFederatedCommunities.length} dbCommunities`)
for (const dbCom of dbFederatedCommunities) {
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',
if (!apiValueStrings.includes(dbCom.apiVersion)) {
logger.warn('Federation: dbCom with unsupported apiVersion', dbCom.endPoint, dbCom.apiVersion)
continue
}
try {
const client = Client.getInstance(dbCom)
const pubKey = await client?.getPublicKey()
if (pubKey && pubKey === dbCom.publicKey.toString()) {
await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() })
logger.info('Federation: verified community', dbCom)
} else {
logger.warn(
'Federation: received not matching publicKey:',
pubKey,
`${dbCom.endPoint}/${dbCom.apiVersion}`,
dbCom.publicKey.toString(),
)
if (pubKey && pubKey === dbCom.publicKey.toString()) {
logger.info(`Federation: matching publicKey: ${pubKey}`)
await DbCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() })
logger.debug(`Federation: updated dbCom: ${JSON.stringify(dbCom)}`)
} else {
logger.warn(
`Federation: received not matching publicKey -> received: ${
pubKey || 'null'
}, expected: ${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: DbCommunity): 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

@ -1,8 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { User } from '@entity/User'
import { AuthChecker } from 'type-graphql'
@ -10,13 +5,14 @@ import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS'
import { decode, encode } from '@/auth/JWT'
import { RIGHTS } from '@/auth/RIGHTS'
import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN } from '@/auth/ROLES'
import { Context } from '@/server/context'
import { LogError } from '@/server/LogError'
export const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
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?
@ -25,7 +21,7 @@ export const isAuthorized: AuthChecker<any> = 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')
}
@ -47,12 +43,12 @@ export const isAuthorized: AuthChecker<any> = 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

@ -6,14 +6,12 @@ export class Community {
constructor(dbCom: DbCommunity) {
this.id = dbCom.id
this.foreign = dbCom.foreign
this.publicKey = dbCom.publicKey.toString()
this.url =
(dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/') + dbCom.apiVersion
this.lastAnnouncedAt = dbCom.lastAnnouncedAt
this.verifiedAt = dbCom.verifiedAt
this.lastErrorAt = dbCom.lastErrorAt
this.createdAt = dbCom.createdAt
this.updatedAt = dbCom.updatedAt
this.name = dbCom.name
this.description = dbCom.description
this.url = dbCom.url
this.creationDate = dbCom.creationDate
this.uuid = dbCom.communityUuid
this.authenticatedAt = dbCom.authenticatedAt
}
@Field(() => Int)
@ -22,24 +20,21 @@ export class Community {
@Field(() => Boolean)
foreign: boolean
@Field(() => String)
publicKey: string
@Field(() => String, { nullable: true })
name: string | null
@Field(() => String, { nullable: true })
description: string | null
@Field(() => String)
url: string
@Field(() => Date, { nullable: true })
lastAnnouncedAt: Date | null
creationDate: Date | null
@Field(() => String, { nullable: true })
uuid: string | null
@Field(() => Date, { nullable: true })
verifiedAt: Date | null
@Field(() => Date, { nullable: true })
lastErrorAt: Date | null
@Field(() => Date, { nullable: true })
createdAt: Date | null
@Field(() => Date, { nullable: true })
updatedAt: Date | null
authenticatedAt: Date | null
}

View File

@ -0,0 +1,45 @@
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { ObjectType, Field, Int } from 'type-graphql'
@ObjectType()
export class FederatedCommunity {
constructor(dbCom: DbFederatedCommunity) {
this.id = dbCom.id
this.foreign = dbCom.foreign
this.publicKey = dbCom.publicKey.toString()
this.url =
(dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/') + dbCom.apiVersion
this.lastAnnouncedAt = dbCom.lastAnnouncedAt
this.verifiedAt = dbCom.verifiedAt
this.lastErrorAt = dbCom.lastErrorAt
this.createdAt = dbCom.createdAt
this.updatedAt = dbCom.updatedAt
}
@Field(() => Int)
id: number
@Field(() => Boolean)
foreign: boolean
@Field(() => String)
publicKey: string
@Field(() => String)
url: string
@Field(() => Date, { nullable: true })
lastAnnouncedAt: Date | null
@Field(() => Date, { nullable: true })
verifiedAt: Date | null
@Field(() => Date, { nullable: true })
lastErrorAt: Date | null
@Field(() => Date, { nullable: true })
createdAt: Date | null
@Field(() => Date, { nullable: true })
updatedAt: Date | null
}

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

@ -1,6 +1,12 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Connection } from '@dbTools/typeorm'
import { Community as DbCommunity } from '@entity/Community'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { ApolloServerTestClient } from 'apollo-server-testing'
import { testEnvironment } from '@test/helpers'
@ -19,7 +25,7 @@ beforeAll(async () => {
testEnv = await testEnvironment()
query = testEnv.query
con = testEnv.con
await DbCommunity.clear()
await DbFederatedCommunity.clear()
})
afterAll(async () => {
@ -28,12 +34,12 @@ afterAll(async () => {
describe('CommunityResolver', () => {
describe('getCommunities', () => {
let homeCom1: DbCommunity
let homeCom2: DbCommunity
let homeCom3: DbCommunity
let foreignCom1: DbCommunity
let foreignCom2: DbCommunity
let foreignCom3: DbCommunity
let homeCom1: DbFederatedCommunity
let homeCom2: DbFederatedCommunity
let homeCom3: DbFederatedCommunity
let foreignCom1: DbFederatedCommunity
let foreignCom2: DbFederatedCommunity
let foreignCom3: DbFederatedCommunity
describe('with empty list', () => {
it('returns no community entry', async () => {
@ -51,29 +57,29 @@ describe('CommunityResolver', () => {
beforeEach(async () => {
jest.clearAllMocks()
homeCom1 = DbCommunity.create()
homeCom1 = DbFederatedCommunity.create()
homeCom1.foreign = false
homeCom1.publicKey = Buffer.from('publicKey-HomeCommunity')
homeCom1.apiVersion = '1_0'
homeCom1.endPoint = 'http://localhost/api'
homeCom1.createdAt = new Date()
await DbCommunity.insert(homeCom1)
await DbFederatedCommunity.insert(homeCom1)
homeCom2 = DbCommunity.create()
homeCom2 = DbFederatedCommunity.create()
homeCom2.foreign = false
homeCom2.publicKey = Buffer.from('publicKey-HomeCommunity')
homeCom2.apiVersion = '1_1'
homeCom2.endPoint = 'http://localhost/api'
homeCom2.createdAt = new Date()
await DbCommunity.insert(homeCom2)
await DbFederatedCommunity.insert(homeCom2)
homeCom3 = DbCommunity.create()
homeCom3 = DbFederatedCommunity.create()
homeCom3.foreign = false
homeCom3.publicKey = Buffer.from('publicKey-HomeCommunity')
homeCom3.apiVersion = '2_0'
homeCom3.endPoint = 'http://localhost/api'
homeCom3.createdAt = new Date()
await DbCommunity.insert(homeCom3)
await DbFederatedCommunity.insert(homeCom3)
})
it('returns 3 home-community entries', async () => {
@ -123,29 +129,29 @@ describe('CommunityResolver', () => {
beforeEach(async () => {
jest.clearAllMocks()
foreignCom1 = DbCommunity.create()
foreignCom1 = DbFederatedCommunity.create()
foreignCom1.foreign = true
foreignCom1.publicKey = Buffer.from('publicKey-ForeignCommunity')
foreignCom1.apiVersion = '1_0'
foreignCom1.endPoint = 'http://remotehost/api'
foreignCom1.createdAt = new Date()
await DbCommunity.insert(foreignCom1)
await DbFederatedCommunity.insert(foreignCom1)
foreignCom2 = DbCommunity.create()
foreignCom2 = DbFederatedCommunity.create()
foreignCom2.foreign = true
foreignCom2.publicKey = Buffer.from('publicKey-ForeignCommunity')
foreignCom2.apiVersion = '1_1'
foreignCom2.endPoint = 'http://remotehost/api'
foreignCom2.createdAt = new Date()
await DbCommunity.insert(foreignCom2)
await DbFederatedCommunity.insert(foreignCom2)
foreignCom3 = DbCommunity.create()
foreignCom3 = DbFederatedCommunity.create()
foreignCom3.foreign = true
foreignCom3.publicKey = Buffer.from('publicKey-ForeignCommunity')
foreignCom3.apiVersion = '1_2'
foreignCom3.endPoint = 'http://remotehost/api'
foreignCom3.createdAt = new Date()
await DbCommunity.insert(foreignCom3)
await DbFederatedCommunity.insert(foreignCom3)
})
it('returns 3 home community and 3 foreign community entries', async () => {

View File

@ -1,22 +1,37 @@
import { Community as DbCommunity } from '@entity/Community'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { Resolver, Query, Authorized } from 'type-graphql'
import { Community } from '@model/Community'
import { FederatedCommunity } from '@model/FederatedCommunity'
import { RIGHTS } from '@/auth/RIGHTS'
@Resolver()
export class CommunityResolver {
@Authorized([RIGHTS.COMMUNITIES])
@Query(() => [Community])
async getCommunities(): Promise<Community[]> {
const dbCommunities: DbCommunity[] = await DbCommunity.find({
@Query(() => [FederatedCommunity])
async getCommunities(): Promise<FederatedCommunity[]> {
const dbFederatedCommunities: DbFederatedCommunity[] = await DbFederatedCommunity.find({
order: {
foreign: 'ASC',
createdAt: 'DESC',
lastAnnouncedAt: 'DESC',
},
})
return dbFederatedCommunities.map(
(dbCom: DbFederatedCommunity) => new FederatedCommunity(dbCom),
)
}
@Authorized([RIGHTS.COMMUNITIES])
@Query(() => [Community])
async getCommunitySelections(): Promise<Community[]> {
const dbCommunities: DbCommunity[] = await DbCommunity.find({
order: {
name: 'ASC',
},
})
return dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom))
}
}

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

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { IsNull, getConnection } from '@dbTools/typeorm'
import { Contribution as DbContribution } from '@entity/Contribution'
import { ContributionMessage } from '@entity/ContributionMessage'
@ -44,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 {
@ -229,11 +229,11 @@ export class ContributionResolver {
contributionMessage.createdAt = contributionToUpdate.updatedAt
? contributionToUpdate.updatedAt
: contributionToUpdate.createdAt
const changeMessage = `${contributionToUpdate.contributionDate}
const changeMessage = `${contributionToUpdate.contributionDate.toString()}
---
${contributionToUpdate.memo}
---
${contributionToUpdate.amount}`
${contributionToUpdate.amount.toString()}`
contributionMessage.message = changeMessage
contributionMessage.isModerator = false
contributionMessage.userId = user.id
@ -259,7 +259,7 @@ export class ContributionResolver {
@Ctx() context: Context,
): Promise<Decimal[]> {
logger.info(
`adminCreateContribution(email=${email}, amount=${amount}, memo=${memo}, creationDate=${creationDate})`,
`adminCreateContribution(email=${email}, amount=${amount.toString()}, memo=${memo}, creationDate=${creationDate})`,
)
const clientTimezoneOffset = getClientTimezoneOffset(context)
if (!isValidDateString(creationDate)) {
@ -270,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) {
@ -501,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

@ -0,0 +1,138 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { Event as DbEvent } from '@entity/Event'
import { UserContact } from '@entity/UserContact'
import { GraphQLError } from 'graphql'
import { cleanDB, resetToken, testEnvironment } from '@test/helpers'
import { logger, i18n as localization } from '@test/testSetup'
import { EventType } from '@/event/Events'
import { userFactory } from '@/seeds/factory/user'
import { login, subscribeNewsletter, unsubscribeNewsletter } from '@/seeds/graphql/mutations'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
let testEnv: any, mutate: any, con: any
beforeAll(async () => {
testEnv = await testEnvironment(logger, localization)
mutate = testEnv.mutate
con = testEnv.con
await cleanDB()
})
afterAll(async () => {
await cleanDB()
await con.close()
})
describe('KlicktippResolver', () => {
beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg)
})
afterAll(async () => {
await cleanDB()
})
describe('subscribeNewsletter', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({
mutation: subscribeNewsletter,
})
expect(errorObjects).toEqual([new GraphQLError('401 Unauthorized')])
})
})
describe('authenticated', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(() => {
resetToken()
})
it('calls API', async () => {
const {
data: { subscribeNewsletter: isSubscribed },
}: { data: { subscribeNewsletter: boolean } } = await mutate({
mutation: subscribeNewsletter,
})
expect(isSubscribed).toEqual(true)
})
it('stores the NEWSLETTER_SUBSCRIBE event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.NEWSLETTER_SUBSCRIBE,
affectedUserId: userConatct.user.id,
actingUserId: userConatct.user.id,
}),
)
})
})
})
describe('unsubscribeNewsletter', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({
mutation: unsubscribeNewsletter,
})
expect(errorObjects).toEqual([new GraphQLError('401 Unauthorized')])
})
})
describe('authenticated', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(() => {
resetToken()
})
it('calls API', async () => {
const {
data: { unsubscribeNewsletter: isUnsubscribed },
}: { data: { unsubscribeNewsletter: boolean } } = await mutate({
mutation: unsubscribeNewsletter,
})
expect(isUnsubscribed).toEqual(true)
})
it('stores the NEWSLETTER_UNSUBSCRIBE event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.NEWSLETTER_UNSUBSCRIBE,
affectedUserId: userConatct.user.id,
actingUserId: userConatct.user.id,
}),
)
})
})
})
})

View File

@ -1,33 +1,17 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { Resolver, Query, Authorized, Arg, Mutation, Ctx } from 'type-graphql'
import { Resolver, Authorized, Mutation, Ctx } from 'type-graphql'
import {
getKlickTippUser,
getKlicktippTagMap,
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'
@Resolver()
export class KlicktippResolver {
@Authorized([RIGHTS.GET_KLICKTIPP_USER])
@Query(() => String)
async getKlicktippUser(@Arg('email') email: string): Promise<string> {
return await getKlickTippUser(email)
}
@Authorized([RIGHTS.GET_KLICKTIPP_TAG_MAP])
@Query(() => String)
async getKlicktippTagMap(): Promise<string> {
return await getKlicktippTagMap()
}
@Authorized([RIGHTS.UNSUBSCRIBE_NEWSLETTER])
@Mutation(() => Boolean)
async unsubscribeNewsletter(@Ctx() context: Context): Promise<boolean> {
const user = getUser(context)
await EVENT_NEWSLETTER_UNSUBSCRIBE(user)
return unsubscribe(user.emailContact.email)
}
@ -35,6 +19,7 @@ export class KlicktippResolver {
@Mutation(() => Boolean)
async subscribeNewsletter(@Ctx() context: Context): Promise<boolean> {
const user = getUser(context)
return klicktippSignIn(user.emailContact.email, user.language)
await EVENT_NEWSLETTER_SUBSCRIBE(user)
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),
),
@ -316,6 +323,7 @@ export class TransactionResolver {
}
// TODO this is subject to replay attacks
// --- WHY?
const senderUser = getUser(context)
// validate recipient user
@ -325,9 +333,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

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

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { CONFIG } from './config'
import { startValidateCommunities } from './federation/validateCommunities'
import { createServer } from './server/createServer'
@ -14,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

@ -7,23 +7,9 @@ import { MiddlewareFn } from 'type-graphql'
import { KlickTipp } from '@model/KlickTipp'
import { /* klicktippSignIn, */ getKlickTippUser } from '@/apis/KlicktippController'
import { CONFIG } from '@/config'
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 },
@ -32,15 +18,13 @@ export const klicktippNewsletterStateMiddleware: MiddlewareFn = async (
// eslint-disable-next-line n/callback-return
const result = await next()
let klickTipp = new KlickTipp({ status: 'Unsubscribed' })
if (CONFIG.KLICKTIPP) {
try {
const klickTippUser = await getKlickTippUser(result.email)
if (klickTippUser) {
klickTipp = new KlickTipp(klickTippUser)
}
} catch (err) {
logger.error(`There is no user for (email='${result.email}') ${err}`)
try {
const klickTippUser = await getKlickTippUser(result.email)
if (klickTippUser) {
klickTipp = new KlickTipp(klickTippUser)
}
} catch (err) {
logger.error(`There is no user for (email='${result.email}') ${err}`)
}
result.klickTipp = klickTipp
return result

View File

@ -2,9 +2,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Contribution } from '@entity/Contribution'
import { Transaction } from '@entity/Transaction'
import { ApolloServerTestClient } from 'apollo-server-testing'
@ -12,7 +9,6 @@ import { ApolloServerTestClient } from 'apollo-server-testing'
import { findUserByEmail } from '@/graphql/resolver/UserResolver'
import { CreationInterface } from '@/seeds/creation/CreationInterface'
import { login, createContribution, confirmContribution } from '@/seeds/graphql/mutations'
// import CONFIG from '@/config/index'
export const nMonthsBefore = (date: Date, months = 1): string => {
return new Date(date.getFullYear(), date.getMonth() - months, 1).toISOString()

View File

@ -1,14 +1,14 @@
import { gql } from 'graphql-tag'
export const subscribeNewsletter = gql`
mutation ($email: String!, $language: String!) {
subscribeNewsletter(email: $email, language: $language)
mutation {
subscribeNewsletter
}
`
export const unsubscribeNewsletter = gql`
mutation ($email: String!) {
unsubscribeNewsletter(email: $email)
mutation {
unsubscribeNewsletter
}
`
@ -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

@ -1,10 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { entities } from '@entity/index'
import { createTestClient } from 'apollo-server-testing'
import { name, internet, datatype } from 'faker'
@ -38,15 +31,17 @@ 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)
}
}
const resetEntity = async (entity: any) => {
const [entityTypes] = entities
const resetEntity = async (entity: typeof entityTypes) => {
const items = await entity.find({ withDeleted: true })
if (items.length > 0) {
const ids = items.map((i: any) => i.id)
const ids = items.map((i) => i.id)
await entity.delete(ids)
}
}
@ -59,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...')
@ -78,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

@ -13,6 +13,7 @@ export interface Context {
role?: Role
user?: dbUser
clientTimezoneOffset?: number
gradidoID?: string
// hack to use less DB calls for Balance Resolver
lastTransaction?: dbTransaction
transactionCount?: number

View File

@ -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: Connection
}
export const createServer = async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -34,7 +38,7 @@ export const createServer = async (
// open mysql connection
const con = await connection()
if (!con || !con.isConnected) {
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

@ -5,7 +5,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import clonedeep from 'lodash.clonedeep'
const setHeadersPlugin = {
@ -13,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 {
@ -28,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

@ -11,8 +11,7 @@ export async function retrieveNotRegisteredEmails(): Promise<string[]> {
}
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) {

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

@ -1,12 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* 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/unbound-method */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { entities } from '@entity/index'
import { createTestClient } from 'apollo-server-testing'
@ -15,6 +7,7 @@ import { createServer } from '@/server/createServer'
import { i18n, logger } from './testSetup'
export const headerPushMock = jest.fn((t) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
context.token = t.value
})
@ -29,12 +22,12 @@ 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)
}
}
export const testEnvironment = async (testLogger: any = logger, testI18n: any = i18n) => {
export const testEnvironment = async (testLogger = logger, testI18n = i18n) => {
const server = await createServer(context, testLogger, testI18n)
const con = server.con
const testClient = createTestClient(server.apollo)
@ -43,10 +36,12 @@ export const testEnvironment = async (testLogger: any = logger, testI18n: any =
return { mutate, query, con }
}
export const resetEntity = async (entity: any) => {
const [entityTypes] = entities
export const resetEntity = async (entity: typeof entityTypes) => {
const items = await entity.find({ withDeleted: true })
if (items.length > 0) {
const ids = items.map((i: any) => i.id)
const ids = items.map((i) => i.id)
await entity.delete(ids)
}
}

View File

@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { CONFIG } from '@/config'
import { i18n } from '@/server/localization'
import { backendLogger as logger } from '@/server/logger'
@ -10,7 +8,7 @@ CONFIG.EMAIL_TEST_MODUS = false
jest.setTimeout(1000000)
jest.mock('@/server/logger', () => {
const originalModule = jest.requireActual('@/server/logger')
const originalModule = jest.requireActual<typeof logger>('@/server/logger')
return {
__esModule: true,
...originalModule,
@ -27,7 +25,7 @@ jest.mock('@/server/logger', () => {
})
jest.mock('@/server/localization', () => {
const originalModule = jest.requireActual('@/server/localization')
const originalModule = jest.requireActual<typeof i18n>('@/server/localization')
return {
__esModule: true,
...originalModule,

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,60 @@
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm'
@Entity('communities')
export class Community extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'foreign', type: 'bool', nullable: false, default: true })
foreign: boolean
@Column({ name: 'url', length: 255, nullable: false })
url: string
@Column({ name: 'public_key', type: 'binary', length: 64, nullable: false })
publicKey: Buffer
@Column({
name: 'community_uuid',
type: 'char',
length: 36,
nullable: true,
collation: 'utf8mb4_unicode_ci',
})
communityUuid: string | null
@Column({ name: 'authenticated_at', type: 'datetime', nullable: true })
authenticatedAt: Date | null
@Column({ name: 'name', type: 'varchar', length: 40, nullable: true })
name: string | null
@Column({ name: 'description', type: 'varchar', length: 255, nullable: true })
description: string | null
@CreateDateColumn({ name: 'creation_date', type: 'datetime', nullable: true })
creationDate: Date | null
@CreateDateColumn({
name: 'created_at',
type: 'datetime',
default: () => 'CURRENT_TIMESTAMP(3)',
nullable: false,
})
createdAt: Date
@UpdateDateColumn({
name: 'updated_at',
type: 'datetime',
onUpdate: 'CURRENT_TIMESTAMP(3)',
nullable: true,
})
updatedAt: Date | null
}

View File

@ -0,0 +1,51 @@
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm'
@Entity('federated_communities')
export class FederatedCommunity extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'foreign', type: 'bool', nullable: false, default: true })
foreign: boolean
@Column({ name: 'public_key', type: 'binary', length: 64, default: null, nullable: true })
publicKey: Buffer
@Column({ name: 'api_version', length: 10, nullable: false })
apiVersion: string
@Column({ name: 'end_point', length: 255, nullable: false })
endPoint: string
@Column({ name: 'last_announced_at', type: 'datetime', nullable: true })
lastAnnouncedAt: Date | null
@Column({ name: 'verified_at', type: 'datetime', nullable: true })
verifiedAt: Date | null
@Column({ name: 'last_error_at', type: 'datetime', nullable: true })
lastErrorAt: Date | null
@CreateDateColumn({
name: 'created_at',
type: 'datetime',
default: () => 'CURRENT_TIMESTAMP(3)',
nullable: false,
})
createdAt: Date
@UpdateDateColumn({
name: 'updated_at',
type: 'datetime',
onUpdate: 'CURRENT_TIMESTAMP(3)',
nullable: true,
})
updatedAt: Date | null
}

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 { Community } from './0060-update_communities_table/Community'
export { Community } from './0065-refactor_communities_table/Community'

View File

@ -0,0 +1 @@
export { FederatedCommunity } from './0065-refactor_communities_table/FederatedCommunity'

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

@ -10,6 +10,7 @@ import { Contribution } from './Contribution'
import { Event } from './Event'
import { ContributionMessage } from './ContributionMessage'
import { Community } from './Community'
import { FederatedCommunity } from './FederatedCommunity'
export const entities = [
Community,
@ -17,6 +18,7 @@ export const entities = [
ContributionLink,
ContributionMessage,
Event,
FederatedCommunity,
LoginElopageBuys,
LoginEmailOptIn,
Migration,

View File

@ -0,0 +1,36 @@
/* MIGRATION TO CREATE THE FEDERATION COMMUNITY TABLES
*
* This migration creates the `community` and 'communityfederation' tables in the `apollo` database (`gradido_community`).
*/
/* 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(`RENAME TABLE communities TO federated_communities;`)
await queryFn(`
CREATE TABLE communities (
\`id\` int unsigned NOT NULL AUTO_INCREMENT,
\`foreign\` tinyint(4) NOT NULL DEFAULT 1,
\`url\` varchar(255) NOT NULL,
\`public_key\` binary(64) NOT NULL,
\`community_uuid\` char(36) NULL,
\`authenticated_at\` datetime(3) NULL,
\`name\` varchar(40) NULL,
\`description\` varchar(255) NULL,
\`creation_date\` datetime(3) NULL,
\`created_at\` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
\`updated_at\` datetime(3),
PRIMARY KEY (id),
UNIQUE KEY url_key (url),
UNIQUE KEY uuid_key (community_uuid),
UNIQUE KEY public_key_key (public_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// write downgrade logic as parameter of queryFn
await queryFn(`DROP TABLE communities;`)
await queryFn(`RENAME TABLE federated_communities TO communities;`)
}

View File

@ -0,0 +1,76 @@
/* 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;`,
)
}
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
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

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

@ -3,7 +3,7 @@ import dotenv from 'dotenv'
dotenv.config()
const constants = {
DB_VERSION: '0064-event_rename',
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',

View File

@ -5,7 +5,7 @@ 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'
CONFIG.FEDERATION_DHT_SEED = '64ebcb0e3ad547848fef4197c6e2332f'
@ -261,7 +261,7 @@ describe('federation', () => {
describe('with receiving wrong but tolerated property data', () => {
let jsonArray: any[]
let result: DbCommunity[] = []
let result: DbFederatedCommunity[] = []
beforeAll(async () => {
jest.clearAllMocks()
jsonArray = [
@ -277,7 +277,7 @@ describe('federation', () => {
},
]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
result = await DbCommunity.find({ foreign: true })
result = await DbFederatedCommunity.find({ foreign: true })
})
afterAll(async () => {
@ -523,7 +523,7 @@ describe('federation', () => {
describe('with receiving data of exact max allowed properties length', () => {
let jsonArray: any[]
let result: DbCommunity[] = []
let result: DbFederatedCommunity[] = []
beforeAll(async () => {
jest.clearAllMocks()
jsonArray = [
@ -538,7 +538,7 @@ describe('federation', () => {
{ api: 'toolong api', url: 'some valid url' },
]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
result = await DbCommunity.find({ foreign: true })
result = await DbFederatedCommunity.find({ foreign: true })
})
afterAll(async () => {
@ -570,7 +570,7 @@ describe('federation', () => {
describe('with receiving data of exact max allowed buffer length', () => {
let jsonArray: any[]
let result: DbCommunity[] = []
let result: DbFederatedCommunity[] = []
beforeAll(async () => {
jest.clearAllMocks()
jsonArray = [
@ -592,7 +592,7 @@ describe('federation', () => {
},
]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
result = await DbCommunity.find({ foreign: true })
result = await DbFederatedCommunity.find({ foreign: true })
})
afterAll(async () => {
@ -711,7 +711,7 @@ describe('federation', () => {
})
describe('with proper data', () => {
let result: DbCommunity[] = []
let result: DbFederatedCommunity[] = []
beforeAll(async () => {
jest.clearAllMocks()
await socketEventMocks.data(
@ -728,7 +728,7 @@ describe('federation', () => {
]),
),
)
result = await DbCommunity.find({ foreign: true })
result = await DbFederatedCommunity.find({ foreign: true })
})
afterAll(async () => {

View File

@ -3,7 +3,7 @@
import DHT from '@hyperswarm/dht'
import { logger } from '@/server/logger'
import CONFIG from '@/config'
import { Community as DbCommunity } from '@entity/Community'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
const KEY_SECRET_SEEDBYTES = 32
const getSeed = (): Buffer | null =>
@ -31,7 +31,7 @@ export const startDHT = async (topic: string): Promise<void> => {
logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`)
logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`)
const ownApiVersions = await writeHomeCommunityEnries(keyPair.publicKey)
const ownApiVersions = await writeFederatedHomeCommunityEnries(keyPair.publicKey)
logger.info(`ApiList: ${JSON.stringify(ownApiVersions)}`)
const node = new DHT({ keyPair })
@ -92,9 +92,9 @@ export const startDHT = async (topic: string): Promise<void> => {
}
logger.debug(`upsert with variables=${JSON.stringify(variables)}`)
// this will NOT update the updatedAt column, to distingue between a normal update and the last announcement
await DbCommunity.createQueryBuilder()
await DbFederatedCommunity.createQueryBuilder()
.insert()
.into(DbCommunity)
.into(DbFederatedCommunity)
.values(variables)
.orUpdate({
conflict_target: ['id', 'publicKey', 'apiVersion'],
@ -179,7 +179,7 @@ export const startDHT = async (topic: string): Promise<void> => {
}
}
async function writeHomeCommunityEnries(pubKey: any): Promise<CommunityApi[]> {
async function writeFederatedHomeCommunityEnries(pubKey: any): Promise<CommunityApi[]> {
const homeApiVersions: CommunityApi[] = Object.values(ApiVersionType).map(function (apiEnum) {
const comApi: CommunityApi = {
api: apiEnum,
@ -189,17 +189,17 @@ async function writeHomeCommunityEnries(pubKey: any): Promise<CommunityApi[]> {
})
try {
// first remove privious existing homeCommunity entries
DbCommunity.createQueryBuilder().delete().where({ foreign: false }).execute()
DbFederatedCommunity.createQueryBuilder().delete().where({ foreign: false }).execute()
homeApiVersions.forEach(async function (homeApi) {
const homeCom = new DbCommunity()
const homeCom = new DbFederatedCommunity()
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
await DbCommunity.insert(homeCom)
await DbFederatedCommunity.insert(homeCom)
logger.info(`federation home-community inserted successfully: ${JSON.stringify(homeCom)}`)
})
} catch (err) {

View File

@ -50,6 +50,52 @@ Before starting in describing the details of the federation handshake, some prer
With the federation additional data tables/entities have to be created.
##### 1st Draft
The following diagramms shows the first draft of the possible database-model base on the migration 0063-event_link_fields.ts with 3 steps of migration to reach the required entities. All three diagramms are not exhaustive and are still a base for discussions:
![img](./image/classdiagramm_x-community-readyness_step1.svg)
In the first step the current communities table will be renamed to communities_federation. A new table communities is created. Because of the dynamic in the communities_federation data during dht-federation the relation between both entities will be on the collumn communities.communities_federation_public_key. This relation will allow to read a community-entry including its relation to the multi federation entries per api-version with the public key as identifier.
![img](./image/classdiagramm_x-community-readyness_step2.svg)
The 2nd step is an introduction of the entity accounts between the users and the transactions table. This will cause a separation of the transactions from the users, to avoid possible conflicts or dependencies between local users of the community and remote users of foreign users, who will be part of x-communitiy-transactions.
![img](./image/classdiagramm_x-community-readyness_step3.svg)
The 3rd step will introduce an additional foreign-users and a users_favorites table. A foreign_user could be stored in the existing users-table, but he will not need all the attributes of a home-user, especially he will never gets an AGE-account in this community. The user_favorites entity is designed to buildup the relations between users and foreign_users or in general between all users. This is simply a first idea for a future discussion.
##### 2nd Draft
After team discussion in architecture meeting a second vision draft for database migration is shown in the following picture. Only the concerned tables of the database migration are presented. The three elliptical surroundings shows the different steps, which should be done in separate issues. The model, table namings and columns are not exhaustive and are still a base for further discussions.
![img](./image/class-diagramm_vision-draft2.svg)
**The first step** with renaming the current `communities` table in `communities_federation` and creating a new `communities` table is not changed. More details about motivation and arguments are described above.
**The second step** is changed to migrate the `users `table by creating a new `users_settings` table and shift the most existing attributes from `users `table to it. The criterium for shifting a column to `user_settings` is to keep only those columns in the `users `table, which will be used for "home-users" and "foreign-users". A foreign-user at this point of time is a user of an other community, who is involved in a x-community-transaction as `linked_user`. He will not have the possibility to login to the home-community, because after a x-community-tx only the attributes of the `users `table will be exchanged during the handshake of transaction processing of both communities. Even the `transactions `table will be ready for x-community-tx with this `users `and `user_settings` migration, because it contains a reference to both participants of the transaction. For easier displaying and because of historical reasons it will be a good idea to add the columns `linked_user `and `user `(not shown in diagramm) to the `transactions `table with type varchar(512) to store the valid firstname and lastname of the participants at transaction-date. If one of the participants will change his names, there will be no migration of the transaction data necessary and a transaction-list will present always the currect names even over a long distance.
**The third step** contains a migration for handling accounts and user_correlations. With the new table `gdd_accounts `a new entity will be introduced to support the different types of gradido accounts AGE, GMW and AUF. The second new table `user_correlations `is to buildup the different correlations a user will have:
* user to user correlation like favorites, trustee, etc
* user to account correlation for cashier of a GMW and AUF community account, trustee of children or seniors, etc.
The previous idea to replace the `user_id `with an `account_id` in the `transactions `table will be not necessary with this draft, because it will not cause a benefit and saves a lot refactoring efforts.
##### 3rd Draft
After further discussions about the database-model and the necessary migration steps the team decided to integrate an additional migration step for X-Community-Transaction. The decision base on keeping the goal focus on implementation of the x-community sendCoins feature as soon as possible.
![img](./image/class-diagramm_vision-draft3.svg)
The additional migration step 2 will simply concentrated on the `transactions `table by adding all necessary columns to handle a *x-community-tx* without a previous migration of the `users `table, shown as step 3 in the picture above.
In concequence of these additional columns in the `transactions `table the database-model will be denormalized by containing redundanten columns. But this migration step will reduce the necessary efforts to reach the *x-community sendCoins* feature as soon as possible. On the other side it offers more possibilities to ensure data consitency, because of internal data checks in conjunction with the redundant columns.
The feature *x-community-tx* per *sendCoins* will create several challenges, because there is no technical transaction bracket, which will ensure consistent data in both community databases after all write access are finished. The most favorite concept about handling a x-community-transaction and the upcoming handshake to ensure valid send- and receive-transaction entries in both databases is to follow the two-phase-commit protocol. To avoid blocking the transactions table or user dependent transactions-entries during the two-phase-commit processing the idea of pending_transactions is born. This additional pending_transactions table contains the same columns than the transactions table plus one column named `x_transactions_state`.
##### Community-Entity
Create the new *Community* table to store attributes of the own community. This table is used more like a frame for own community data in the future like the list of federated foreign communities, own users, own futher accounts like AUF- and Welfare-account and the profile data of the own community:

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

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