separated dht-hub features in new dht-node modul

This commit is contained in:
Claus-Peter Hübner 2023-01-03 21:29:21 +01:00
parent 200851322e
commit 17515f1ac0
33 changed files with 4969 additions and 0 deletions

69
dht-node/.env.dist Normal file
View File

@ -0,0 +1,69 @@
CONFIG_VERSION=v14.2022-12-22
# Server
PORT=4000
JWT_SECRET=secret123
JWT_EXPIRES_IN=10m
GRAPHIQL=false
GDT_API_URL=https://gdt.gradido.net
# Database
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=
DB_DATABASE=gradido_community
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
# Klicktipp
KLICKTIPP=false
KLICKTTIPP_API_URL=https://api.klicktipp.com
KLICKTIPP_USER=gradido_test
KLICKTIPP_PASSWORD=secret321
KLICKTIPP_APIKEY_DE=SomeFakeKeyDE
KLICKTIPP_APIKEY_EN=SomeFakeKeyEN
# Community
COMMUNITY_NAME=Gradido Entwicklung
COMMUNITY_URL=http://localhost/
COMMUNITY_REGISTER_URL=http://localhost/register
COMMUNITY_REDEEM_URL=http://localhost/redeem/{code}
COMMUNITY_REDEEM_CONTRIBUTION_URL=http://localhost/redeem/CL-{code}
COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido.
COMMUNITY_SUPPORT_MAIL=support@supportmail.com
# Login Server
LOGIN_APP_SECRET=21ffbbc616fe
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
# EMail
EMAIL=false
EMAIL_TEST_MODUS=false
EMAIL_TEST_RECEIVER=stage1@gradido.net
EMAIL_USERNAME=gradido_email
EMAIL_SENDER=info@gradido.net
EMAIL_PASSWORD=xxx
EMAIL_SMTP_URL=gmail.com
EMAIL_SMTP_PORT=587
EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{optin}{code}
EMAIL_LINK_SETPASSWORD=http://localhost/reset-password/{optin}
EMAIL_LINK_FORGOTPASSWORD=http://localhost/forgot-password
EMAIL_LINK_OVERVIEW=http://localhost/overview
EMAIL_CODE_VALID_TIME=1440
EMAIL_CODE_REQUEST_TIME=10
# Webhook
WEBHOOK_ELOPAGE_SECRET=secret
# EventProtocol
EVENT_PROTOCOL_DISABLED=false
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
# LOG_LEVEL=info
# Federation
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
# on an hash created from this topic
# FEDERATION_DHT_TOPIC=GRADIDO_HUB
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f

63
dht-node/.env.template Normal file
View File

@ -0,0 +1,63 @@
CONFIG_VERSION=$BACKEND_CONFIG_VERSION
# Server
JWT_SECRET=$JWT_SECRET
JWT_EXPIRES_IN=$JWT_EXPIRES_IN
GRAPHIQL=false
GDT_API_URL=$GDT_API_URL
# Database
DB_HOST=localhost
DB_PORT=3306
DB_USER=$DB_USER
DB_PASSWORD=$DB_PASSWORD
DB_DATABASE=gradido_community
TYPEORM_LOGGING_RELATIVE_PATH=$TYPEORM_LOGGING_RELATIVE_PATH
# Klicktipp
KLICKTIPP=$KLICKTIPP
KLICKTTIPP_API_URL=https://api.klicktipp.com
KLICKTIPP_USER=$KLICKTIPP_USER
KLICKTIPP_PASSWORD=$KLICKTIPP_PASSWORD
KLICKTIPP_APIKEY_DE=$KLICKTIPP_APIKEY_DE
KLICKTIPP_APIKEY_EN=$KLICKTIPP_APIKEY_EN
# Community
COMMUNITY_NAME=$COMMUNITY_NAME
COMMUNITY_URL=$COMMUNITY_URL
COMMUNITY_REGISTER_URL=$COMMUNITY_REGISTER_URL
COMMUNITY_REDEEM_URL=$COMMUNITY_REDEEM_URL
COMMUNITY_REDEEM_CONTRIBUTION_URL=$COMMUNITY_REDEEM_CONTRIBUTION_URL
COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION
COMMUNITY_SUPPORT_MAIL=$COMMUNITY_SUPPORT_MAIL
# Login Server
LOGIN_APP_SECRET=21ffbbc616fe
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
# EMail
EMAIL=$EMAIL
EMAIL_TEST_MODUS=$EMAIL_TEST_MODUS
EMAIL_TEST_RECEIVER=$EMAIL_TEST_RECEIVER
EMAIL_USERNAME=$EMAIL_USERNAME
EMAIL_SENDER=$EMAIL_SENDER
EMAIL_PASSWORD=$EMAIL_PASSWORD
EMAIL_SMTP_URL=$EMAIL_SMTP_URL
EMAIL_SMTP_PORT=587
EMAIL_LINK_VERIFICATION=$EMAIL_LINK_VERIFICATION
EMAIL_LINK_SETPASSWORD=$EMAIL_LINK_SETPASSWORD
EMAIL_LINK_FORGOTPASSWORD=$EMAIL_LINK_FORGOTPASSWORD
EMAIL_LINK_OVERVIEW=$EMAIL_LINK_OVERVIEW
EMAIL_CODE_VALID_TIME=$EMAIL_CODE_VALID_TIME
EMAIL_CODE_REQUEST_TIME=$EMAIL_CODE_REQUEST_TIME
# Webhook
WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET
# EventProtocol
EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED
# Federation
FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC
FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED
FEDERATION_COMMUNITY_URL=$FEDERATION_COMMUNITY_URL

8
dht-node/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
/node_modules/
/.env
/.env.bak
/build/
package-json.lock
coverage
# emacs
*~

146
dht-node/log4js-config.json Normal file
View File

@ -0,0 +1,146 @@
{
"appenders":
{
"access":
{
"type": "dateFile",
"filename": "../logs/federation/access-%p.log",
"pattern": "yyyy-MM-dd",
"layout":
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
},
"keepFileExt" : true,
"fileNameSep" : "_",
"numBackups" : 30
},
"apollo":
{
"type": "dateFile",
"filename": "../logs/federation/apollo-%p.log",
"pattern": "yyyy-MM-dd",
"layout":
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
},
"keepFileExt" : true,
"fileNameSep" : "_",
"numBackups" : 30
},
"backend":
{
"type": "dateFile",
"filename": "../logs/federation/backend-%p.log",
"pattern": "yyyy-MM-dd",
"layout":
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
},
"keepFileExt" : true,
"fileNameSep" : "_",
"numBackups" : 30
},
"federation":
{
"type": "dateFile",
"filename": "../logs/federation/apiversion-%v-%p.log",
"pattern": "yyyy-MM-dd",
"layout":
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
},
"keepFileExt" : true,
"fileNameSep" : "_",
"numBackups" : 30
},
"errorFile":
{
"type": "dateFile",
"filename": "../logs/federation/errors-%p.log",
"pattern": "yyyy-MM-dd",
"layout":
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
},
"keepFileExt" : true,
"fileNameSep" : "_",
"numBackups" : 30
},
"errors":
{
"type": "logLevelFilter",
"level": "error",
"appender": "errorFile"
},
"out":
{
"type": "stdout",
"layout":
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
}
},
"apolloOut":
{
"type": "stdout",
"layout":
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
}
}
},
"categories":
{
"default":
{
"appenders":
[
"out",
"errors"
],
"level": "debug",
"enableCallStack": true
},
"apollo":
{
"appenders":
[
"apollo",
"apolloOut",
"errors"
],
"level": "debug",
"enableCallStack": true
},
"backend":
{
"appenders":
[
"backend",
"out",
"errors"
],
"level": "debug",
"enableCallStack": true
},
"federation":
{
"appenders":
[
"federation",
"out",
"errors"
],
"level": "debug",
"enableCallStack": true
},
"http":
{
"appenders":
[
"access"
],
"level": "info"
}
}
}

42
dht-node/package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "gradido-federation",
"version": "1.0.0",
"description": "Gradido federation module providing Gradido-Hub-Federation and versioned API for inter community communication",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/federation",
"author": "Claus-Peter Huebner",
"license": "Apache-2.0",
"private": false,
"scripts": {
"build": "tsc --build",
"clean": "tsc --build --clean",
"start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js",
"dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r dotenv/config -r tsconfig-paths/register src/index.ts",
"lint": "eslint --max-warnings=0 --ext .js,.ts ."
},
"dependencies": {
"@hyperswarm/dht": "^6.3.3",
"@types/dotenv": "^8.2.0",
"@types/i18n": "^0.13.6",
"@types/jsonwebtoken": "^8.5.9",
"@types/lodash.clonedeep": "^4.5.7",
"@types/node": "^18.11.11",
"apollo-server-express": "2.25.2",
"class-validator": "^0.13.2",
"cross-env": "^7.0.3",
"decimal.js-light": "^2.5.1",
"dotenv": "10.0.0",
"express": "4.17.1",
"graphql": "15.5.1",
"i18n": "0.15.1",
"jsonwebtoken": "^8.5.1",
"lodash.clonedeep": "^4.5.0",
"log4js": "^6.7.1",
"nodemon": "^2.0.20",
"reflect-metadata": "^0.1.13",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.1",
"type-graphql": "^1.1.1",
"typescript": "^4.9.3"
}
}

View File

@ -0,0 +1,5 @@
import { JwtPayload } from 'jsonwebtoken'
export interface CustomJwtPayload extends JwtPayload {
pubKey: Buffer
}

View File

@ -0,0 +1,12 @@
import { RIGHTS } from './RIGHTS'
export const INALIENABLE_RIGHTS = [
RIGHTS.LOGIN,
RIGHTS.GET_COMMUNITY_INFO,
RIGHTS.COMMUNITIES,
RIGHTS.CREATE_USER,
RIGHTS.SEND_RESET_PASSWORD_EMAIL,
RIGHTS.SET_PASSWORD,
RIGHTS.QUERY_TRANSACTION_LINK,
RIGHTS.QUERY_OPT_IN,
]

19
dht-node/src/auth/JWT.ts Normal file
View File

@ -0,0 +1,19 @@
import jwt from 'jsonwebtoken'
import CONFIG from '@/config/'
import { CustomJwtPayload } from './CustomJwtPayload'
export const decode = (token: string): CustomJwtPayload | null => {
if (!token) throw new Error('401 Unauthorized')
try {
return <CustomJwtPayload>jwt.verify(token, CONFIG.JWT_SECRET)
} catch (err) {
return null
}
}
export const encode = (pubKey: Buffer): string => {
const token = jwt.sign({ pubKey }, CONFIG.JWT_SECRET, {
expiresIn: CONFIG.JWT_EXPIRES_IN,
})
return token
}

View File

@ -0,0 +1,56 @@
export enum RIGHTS {
LOGIN = 'LOGIN',
VERIFY_LOGIN = 'VERIFY_LOGIN',
BALANCE = 'BALANCE',
GET_COMMUNITY_INFO = 'GET_COMMUNITY_INFO',
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',
SEND_COINS = 'SEND_COINS',
LOGOUT = 'LOGOUT',
CREATE_USER = 'CREATE_USER',
SEND_RESET_PASSWORD_EMAIL = 'SEND_RESET_PASSWORD_EMAIL',
SET_PASSWORD = 'SET_PASSWORD',
QUERY_OPT_IN = 'QUERY_OPT_IN',
UPDATE_USER_INFOS = 'UPDATE_USER_INFOS',
HAS_ELOPAGE = 'HAS_ELOPAGE',
CREATE_TRANSACTION_LINK = 'CREATE_TRANSACTION_LINK',
DELETE_TRANSACTION_LINK = 'DELETE_TRANSACTION_LINK',
QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK',
REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK',
LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS',
GDT_BALANCE = 'GDT_BALANCE',
CREATE_CONTRIBUTION = 'CREATE_CONTRIBUTION',
DELETE_CONTRIBUTION = 'DELETE_CONTRIBUTION',
LIST_CONTRIBUTIONS = 'LIST_CONTRIBUTIONS',
LIST_ALL_CONTRIBUTIONS = 'LIST_ALL_CONTRIBUTIONS',
UPDATE_CONTRIBUTION = 'UPDATE_CONTRIBUTION',
LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS',
COMMUNITY_STATISTICS = 'COMMUNITY_STATISTICS',
SEARCH_ADMIN_USERS = 'SEARCH_ADMIN_USERS',
CREATE_CONTRIBUTION_MESSAGE = 'CREATE_CONTRIBUTION_MESSAGE',
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
// Admin
SEARCH_USERS = 'SEARCH_USERS',
SET_USER_ROLE = 'SET_USER_ROLE',
DELETE_USER = 'DELETE_USER',
UNDELETE_USER = 'UNDELETE_USER',
ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION',
ADMIN_CREATE_CONTRIBUTIONS = 'ADMIN_CREATE_CONTRIBUTIONS',
ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION',
LIST_UNCONFIRMED_CONTRIBUTIONS = 'LIST_UNCONFIRMED_CONTRIBUTIONS',
CONFIRM_CONTRIBUTION = 'CONFIRM_CONTRIBUTION',
SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL',
CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST',
LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN',
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
}

View File

@ -0,0 +1,40 @@
import { INALIENABLE_RIGHTS } from './INALIENABLE_RIGHTS'
import { RIGHTS } from './RIGHTS'
import { Role } from './Role'
export const ROLE_UNAUTHORIZED = new Role('unauthorized', INALIENABLE_RIGHTS)
export const ROLE_USER = new Role('user', [
...INALIENABLE_RIGHTS,
RIGHTS.VERIFY_LOGIN,
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,
RIGHTS.SEND_COINS,
RIGHTS.LOGOUT,
RIGHTS.UPDATE_USER_INFOS,
RIGHTS.HAS_ELOPAGE,
RIGHTS.CREATE_TRANSACTION_LINK,
RIGHTS.DELETE_TRANSACTION_LINK,
RIGHTS.REDEEM_TRANSACTION_LINK,
RIGHTS.LIST_TRANSACTION_LINKS,
RIGHTS.GDT_BALANCE,
RIGHTS.CREATE_CONTRIBUTION,
RIGHTS.DELETE_CONTRIBUTION,
RIGHTS.LIST_CONTRIBUTIONS,
RIGHTS.LIST_ALL_CONTRIBUTIONS,
RIGHTS.UPDATE_CONTRIBUTION,
RIGHTS.SEARCH_ADMIN_USERS,
RIGHTS.LIST_CONTRIBUTION_LINKS,
RIGHTS.COMMUNITY_STATISTICS,
RIGHTS.CREATE_CONTRIBUTION_MESSAGE,
RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES,
])
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
// TODO from database
export const ROLES = [ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN]

15
dht-node/src/auth/Role.ts Normal file
View File

@ -0,0 +1,15 @@
import { RIGHTS } from './RIGHTS'
export class Role {
id: string
rights: RIGHTS[]
constructor(id: string, rights: RIGHTS[]) {
this.id = id
this.rights = rights
}
hasRight = (right: RIGHTS): boolean => {
return this.rights.includes(right)
}
}

View File

@ -0,0 +1,96 @@
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
import dotenv from 'dotenv'
dotenv.config()
/*
import Decimal from 'decimal.js-light'
Decimal.set({
precision: 25,
rounding: Decimal.ROUND_HALF_UP,
})
*/
const constants = {
DB_VERSION: '0058-add_communities_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',
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v14.2022-11-22',
CURRENT: '',
},
}
const server = {
PORT: process.env.PORT || 5000,
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',
PRODUCTION: process.env.NODE_ENV === 'production' || false,
}
const database = {
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',
}
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_REDEEM_CONTRIBUTION_URL:
process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}',
COMMUNITY_DESCRIPTION:
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
}
const eventProtocol = {
// global switch to enable writing of EventProtocol-Entries
EVENT_PROTOCOL_DISABLED: process.env.EVENT_PROTOCOL_DISABLED === 'true' || false,
}
// 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
if (
![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes(
constants.CONFIG_VERSION.CURRENT,
)
) {
throw new Error(
`Fatal: Config Version incorrect - expected "${constants.CONFIG_VERSION.EXPECTED}" or "${constants.CONFIG_VERSION.DEFAULT}", but found "${constants.CONFIG_VERSION.CURRENT}"`,
)
}
const federation = {
FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null,
FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
FEDERATION_PORT: process.env.FEDERATION_PORT || 5000,
FEDERATION_API: process.env.FEDERATION_API || '1_0',
FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL || null,
}
const CONFIG = {
...constants,
...server,
...database,
//...klicktipp,
...community,
//...email,
//...loginServer,
//...webhook,
//...eventProtocol,
...federation,
}
export default CONFIG

View File

@ -0,0 +1 @@
declare module '@hyperswarm/dht'

View File

@ -0,0 +1,798 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
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 { testEnvironment, cleanDB } from '@test/helpers'
CONFIG.FEDERATION_DHT_SEED = '64ebcb0e3ad547848fef4197c6e2332f'
jest.mock('@hyperswarm/dht')
const TEST_TOPIC = 'gradido_test_topic'
const keyPairMock = {
publicKey: Buffer.from('publicKey'),
secretKey: Buffer.from('secretKey'),
}
const serverListenSpy = jest.fn()
const serverEventMocks: { [key: string]: any } = {}
const serverOnMock = jest.fn().mockImplementation((key: string, callback) => {
serverEventMocks[key] = callback
})
const nodeCreateServerMock = jest.fn().mockImplementation(() => {
return {
on: serverOnMock,
listen: serverListenSpy,
}
})
const nodeAnnounceMock = jest.fn().mockImplementation(() => {
return {
finished: jest.fn(),
}
})
const lookupResultMock = {
token: Buffer.from(TEST_TOPIC),
from: {
id: Buffer.from('somone'),
host: '188.95.53.5',
port: 63561,
},
to: { id: null, host: '83.53.31.27', port: 55723 },
peers: [
{
publicKey: Buffer.from('some-public-key'),
relayAddresses: [],
},
],
}
const nodeLookupMock = jest.fn().mockResolvedValue([lookupResultMock])
const socketEventMocks: { [key: string]: any } = {}
const socketOnMock = jest.fn().mockImplementation((key: string, callback) => {
socketEventMocks[key] = callback
})
const socketWriteMock = jest.fn()
const nodeConnectMock = jest.fn().mockImplementation(() => {
return {
on: socketOnMock,
once: socketOnMock,
write: socketWriteMock,
}
})
DHT.hash.mockImplementation(() => {
return Buffer.from(TEST_TOPIC)
})
DHT.keyPair.mockImplementation(() => {
return keyPairMock
})
DHT.mockImplementation(() => {
return {
createServer: nodeCreateServerMock,
announce: nodeAnnounceMock,
lookup: nodeLookupMock,
connect: nodeConnectMock,
}
})
let con: any
let testEnv: any
beforeAll(async () => {
testEnv = await testEnvironment(logger)
con = testEnv.con
await cleanDB()
})
afterAll(async () => {
await cleanDB()
await con.close()
})
describe('federation', () => {
beforeAll(() => {
jest.useFakeTimers()
})
describe('call startDHT', () => {
const hashSpy = jest.spyOn(DHT, 'hash')
const keyPairSpy = jest.spyOn(DHT, 'keyPair')
beforeEach(async () => {
DHT.mockClear()
jest.clearAllMocks()
await startDHT(TEST_TOPIC)
})
it('calls DHT.hash', () => {
expect(hashSpy).toBeCalledWith(Buffer.from(TEST_TOPIC))
})
it('creates a key pair', () => {
expect(keyPairSpy).toBeCalledWith(expect.any(Buffer))
})
it('initializes a new DHT object', () => {
expect(DHT).toBeCalledWith({ keyPair: keyPairMock })
})
describe('DHT node', () => {
it('creates a server', () => {
expect(nodeCreateServerMock).toBeCalled()
})
it('listens on the server', () => {
expect(serverListenSpy).toBeCalled()
})
describe('timers', () => {
beforeEach(() => {
jest.runOnlyPendingTimers()
})
it('announces on topic', () => {
expect(nodeAnnounceMock).toBeCalledWith(Buffer.from(TEST_TOPIC), keyPairMock)
})
it('looks up on topic', () => {
expect(nodeLookupMock).toBeCalledWith(Buffer.from(TEST_TOPIC))
})
})
describe('server connection event', () => {
beforeEach(() => {
serverEventMocks.connection({
remotePublicKey: Buffer.from('another-public-key'),
on: socketOnMock,
})
})
it('can be triggered', () => {
expect(socketOnMock).toBeCalled()
})
describe('socket events', () => {
describe('on data', () => {
describe('with receiving simply a string', () => {
beforeEach(() => {
jest.clearAllMocks()
socketEventMocks.data(Buffer.from('no-json string'))
})
it('logs the received data', () => {
expect(logger.info).toBeCalledWith('data: no-json string')
})
it('logs an error of unexpected data format and structure', () => {
expect(logger.error).toBeCalledWith(
'Error on receiving data from socket:',
new SyntaxError('Unexpected token o in JSON at position 1'),
)
})
})
describe('with receiving array of strings', () => {
beforeEach(() => {
jest.clearAllMocks()
const strArray: string[] = ['invalid type test', 'api', 'url']
socketEventMocks.data(Buffer.from(strArray.toString()))
})
it('logs the received data', () => {
expect(logger.info).toBeCalledWith('data: invalid type test,api,url')
})
it('logs an error of unexpected data format and structure', () => {
expect(logger.error).toBeCalledWith(
'Error on receiving data from socket:',
new SyntaxError('Unexpected token i in JSON at position 0'),
)
})
})
describe('with receiving array of string-arrays', () => {
beforeEach(async () => {
jest.clearAllMocks()
const strArray: string[][] = [
[`api`, `url`, `invalid type in array test`],
[`wrong`, `api`, `url`],
]
await socketEventMocks.data(Buffer.from(strArray.toString()))
})
it('logs the received data', () => {
expect(logger.info).toBeCalledWith(
'data: api,url,invalid type in array test,wrong,api,url',
)
})
it('logs an error of unexpected data format and structure', () => {
expect(logger.error).toBeCalledWith(
'Error on receiving data from socket:',
new SyntaxError('Unexpected token a in JSON at position 0'),
)
})
})
describe('with receiving JSON-Array with too much entries', () => {
let jsonArray: { api: string; url: string }[]
beforeEach(async () => {
jest.clearAllMocks()
jsonArray = [
{ api: 'v1_0', url: 'too much versions at the same time test' },
{ api: 'v1_0', url: 'url2' },
{ api: 'v1_0', url: 'url3' },
{ api: 'v1_0', url: 'url4' },
{ api: 'v1_0', url: 'url5' },
]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
})
it('logs the received data', () => {
expect(logger.info).toBeCalledWith(
'data: [{"api":"v1_0","url":"too much versions at the same time test"},{"api":"v1_0","url":"url2"},{"api":"v1_0","url":"url3"},{"api":"v1_0","url":"url4"},{"api":"v1_0","url":"url5"}]',
)
})
it('logs a warning of too much apiVersion-Definitions', () => {
expect(logger.warn).toBeCalledWith(
`received totaly wrong or too much apiVersions-Definition JSON-String: ${JSON.stringify(
jsonArray,
)}`,
)
})
})
describe('with receiving wrong but tolerated property data', () => {
let jsonArray: any[]
let result: DbCommunity[] = []
beforeAll(async () => {
jest.clearAllMocks()
jsonArray = [
{
wrong: 'wrong but tolerated property test',
api: 'v1_0',
url: 'url1',
},
{
api: 'v2_0',
url: 'url2',
wrong: 'wrong but tolerated property test',
},
]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
result = await DbCommunity.find()
})
afterAll(async () => {
await cleanDB()
})
it('has two Communty entries in database', () => {
expect(result).toHaveLength(2)
})
it('has an entry for api version v1_0', () => {
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
publicKey: expect.any(Buffer),
apiVersion: 'v1_0',
endPoint: 'url1',
lastAnnouncedAt: expect.any(Date),
createdAt: expect.any(Date),
updatedAt: null,
}),
]),
)
})
it('has an entry for api version v2_0', () => {
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
publicKey: expect.any(Buffer),
apiVersion: 'v2_0',
endPoint: 'url2',
lastAnnouncedAt: expect.any(Date),
createdAt: expect.any(Date),
updatedAt: null,
}),
]),
)
})
})
describe('with receiving data but missing api property', () => {
let jsonArray: any[]
beforeEach(async () => {
jest.clearAllMocks()
jsonArray = [
{ test1: 'missing api proterty test', url: 'any url definition as string' },
{ api: 'some api', test2: 'missing url property test' },
]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
})
it('logs the received data', () => {
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
})
it('logs a warning of invalid apiVersion-Definition', () => {
expect(logger.warn).toBeCalledWith(
`received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`,
)
})
})
describe('with receiving data but missing url property', () => {
let jsonArray: any[]
beforeEach(async () => {
jest.clearAllMocks()
jsonArray = [
{ api: 'some api', test2: 'missing url property test' },
{ test1: 'missing api proterty test', url: 'any url definition as string' },
]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
})
it('logs the received data', () => {
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
})
it('logs a warning of invalid apiVersion-Definition', () => {
expect(logger.warn).toBeCalledWith(
`received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`,
)
})
})
describe('with receiving data but wrong type of api property', () => {
let jsonArray: any[]
beforeEach(async () => {
jest.clearAllMocks()
jsonArray = [
{ api: 1, url: 'wrong property type tests' },
{ api: 'urltyptest', url: 2 },
{ api: 1, url: 2 },
]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
})
it('logs the received data', () => {
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
})
it('logs a warning of invalid apiVersion-Definition', () => {
expect(logger.warn).toBeCalledWith(
`received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`,
)
})
})
describe('with receiving data but wrong type of url property', () => {
let jsonArray: any[]
beforeEach(async () => {
jest.clearAllMocks()
jsonArray = [
{ api: 'urltyptest', url: 2 },
{ api: 1, url: 'wrong property type tests' },
{ api: 1, url: 2 },
]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
})
it('logs the received data', () => {
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
})
it('logs a warning of invalid apiVersion-Definition', () => {
expect(logger.warn).toBeCalledWith(
`received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`,
)
})
})
describe('with receiving data but wrong type of both properties', () => {
let jsonArray: any[]
beforeEach(async () => {
jest.clearAllMocks()
jsonArray = [
{ api: 1, url: 2 },
{ api: 'urltyptest', url: 2 },
{ api: 1, url: 'wrong property type tests' },
]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
})
it('logs the received data', () => {
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
})
it('logs a warning of invalid apiVersion-Definition', () => {
expect(logger.warn).toBeCalledWith(
`received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`,
)
})
})
describe('with receiving data but too long api string', () => {
let jsonArray: any[]
beforeEach(async () => {
jest.clearAllMocks()
jsonArray = [
{ api: 'toolong api', url: 'some valid url' },
{
api: 'valid api',
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
},
{
api: 'toolong api',
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
},
]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
})
it('logs the received data', () => {
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
})
it('logs a warning of invalid apiVersion-Definition', () => {
expect(logger.warn).toBeCalledWith(
`received apiVersion with content longer than max length: ${JSON.stringify(
jsonArray[0],
)}`,
)
})
})
describe('with receiving data but too long url string', () => {
let jsonArray: any[]
beforeEach(async () => {
jest.clearAllMocks()
jsonArray = [
{
api: 'api',
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
},
{ api: 'toolong api', url: 'some valid url' },
{
api: 'toolong api',
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
},
]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
})
it('logs the received data', () => {
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
})
it('logs a warning of invalid apiVersion-Definition', () => {
expect(logger.warn).toBeCalledWith(
`received apiVersion with content longer than max length: ${JSON.stringify(
jsonArray[0],
)}`,
)
})
})
describe('with receiving data but both properties with too long strings', () => {
let jsonArray: any[]
beforeEach(async () => {
jest.clearAllMocks()
jsonArray = [
{
api: 'toolong api',
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
},
{
api: 'api',
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
},
{ api: 'toolong api', url: 'some valid url' },
]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
})
it('logs the received data', () => {
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
})
})
describe('with receiving data of exact max allowed properties length', () => {
let jsonArray: any[]
let result: DbCommunity[] = []
beforeAll(async () => {
jest.clearAllMocks()
jsonArray = [
{
api: 'valid api',
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
},
{
api: 'api',
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
},
{ api: 'toolong api', url: 'some valid url' },
]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
result = await DbCommunity.find()
})
afterAll(async () => {
await cleanDB()
})
it('has one Communty entry in database', () => {
expect(result).toHaveLength(1)
})
it(`has an entry with max content length for api and url`, () => {
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
publicKey: expect.any(Buffer),
apiVersion: 'valid api',
endPoint:
'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
lastAnnouncedAt: expect.any(Date),
createdAt: expect.any(Date),
updatedAt: null,
}),
]),
)
})
})
describe('with receiving data of exact max allowed buffer length', () => {
let jsonArray: any[]
let result: DbCommunity[] = []
beforeAll(async () => {
jest.clearAllMocks()
jsonArray = [
{
api: 'valid api1',
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
},
{
api: 'valid api2',
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
},
{
api: 'valid api3',
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
},
{
api: 'valid api4',
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
},
]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
result = await DbCommunity.find()
})
afterAll(async () => {
await cleanDB()
})
it('has five Communty entries in database', () => {
expect(result).toHaveLength(4)
})
it(`has an entry 'valid api1' with max content length for api and url`, () => {
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
publicKey: expect.any(Buffer),
apiVersion: 'valid api1',
endPoint:
'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
lastAnnouncedAt: expect.any(Date),
createdAt: expect.any(Date),
updatedAt: null,
}),
]),
)
})
it(`has an entry 'valid api2' with max content length for api and url`, () => {
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
publicKey: expect.any(Buffer),
apiVersion: 'valid api2',
endPoint:
'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
lastAnnouncedAt: expect.any(Date),
createdAt: expect.any(Date),
updatedAt: null,
}),
]),
)
})
it(`has an entry 'valid api3' with max content length for api and url`, () => {
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
publicKey: expect.any(Buffer),
apiVersion: 'valid api3',
endPoint:
'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
lastAnnouncedAt: expect.any(Date),
createdAt: expect.any(Date),
updatedAt: null,
}),
]),
)
})
it(`has an entry 'valid api4' with max content length for api and url`, () => {
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
publicKey: expect.any(Buffer),
apiVersion: 'valid api4',
endPoint:
'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
lastAnnouncedAt: expect.any(Date),
createdAt: expect.any(Date),
updatedAt: null,
}),
]),
)
})
})
describe('with receiving data longer than max allowed buffer length', () => {
let jsonArray: any[]
beforeEach(async () => {
jest.clearAllMocks()
jsonArray = [
{
api: 'Xvalid api1',
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
},
{
api: 'valid api2',
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
},
{
api: 'valid api3',
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
},
{
api: 'valid api4',
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
},
]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
})
it('logs the received data', () => {
expect(logger.warn).toBeCalledWith(
`received more than max allowed length of data buffer: ${
JSON.stringify(jsonArray).length
} against 1141 max allowed`,
)
})
})
describe('with proper data', () => {
let result: DbCommunity[] = []
beforeAll(async () => {
jest.clearAllMocks()
await socketEventMocks.data(
Buffer.from(
JSON.stringify([
{
api: 'v1_0',
url: 'http://localhost:4000/api/v1_0',
},
{
api: 'v2_0',
url: 'http://localhost:4000/api/v2_0',
},
]),
),
)
result = await DbCommunity.find()
})
afterAll(async () => {
await cleanDB()
})
it('has two Communty entries in database', () => {
expect(result).toHaveLength(2)
})
it('has an entry for api version v1_0', () => {
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
publicKey: expect.any(Buffer),
apiVersion: 'v1_0',
endPoint: 'http://localhost:4000/api/v1_0',
lastAnnouncedAt: expect.any(Date),
createdAt: expect.any(Date),
updatedAt: null,
}),
]),
)
})
it('has an entry for api version v2_0', () => {
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
publicKey: expect.any(Buffer),
apiVersion: 'v2_0',
endPoint: 'http://localhost:4000/api/v2_0',
lastAnnouncedAt: expect.any(Date),
createdAt: expect.any(Date),
updatedAt: null,
}),
]),
)
})
})
})
describe('on open', () => {
beforeEach(() => {
socketEventMocks.open()
})
it.skip('calls socket write with own api versions', () => {
expect(socketWriteMock).toBeCalledWith(
Buffer.from(
JSON.stringify([
{
api: 'v1_0',
url: 'http://localhost:4000/api/v1_0',
},
{
api: 'v1_1',
url: 'http://localhost:4000/api/v1_1',
},
{
api: 'v2_0',
url: 'http://localhost:4000/api/v2_0',
},
]),
),
)
})
})
})
})
})
})
})

View File

@ -0,0 +1,185 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import DHT from '@hyperswarm/dht'
import { federationLogger as logger } from '@/server/logger'
import CONFIG from '@/config'
import { Community as DbCommunity } from '@entity/Community'
const KEY_SECRET_SEEDBYTES = 32
const getSeed = (): Buffer | null =>
CONFIG.FEDERATION_DHT_SEED ? Buffer.alloc(KEY_SECRET_SEEDBYTES, CONFIG.FEDERATION_DHT_SEED) : null
const POLLTIME = 20000
const SUCCESSTIME = 120000
const ERRORTIME = 240000
const ANNOUNCETIME = 30000
enum ApiVersionType {
V1_0 = 'v1_0',
V1_1 = 'v1_1',
V2_0 = 'v2_0',
}
type CommunityApi = {
api: string
url: string
}
export const startDHT = async (topic: string): Promise<void> => {
try {
const TOPIC = DHT.hash(Buffer.from(topic))
const keyPair = DHT.keyPair(getSeed())
logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`)
logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`)
const ownApiVersions = Object.values(ApiVersionType).map(function (apiEnum) {
const comApi: CommunityApi = {
api: apiEnum,
url: CONFIG.FEDERATION_COMMUNITY_URL + apiEnum,
}
return comApi
})
logger.debug(`ApiList: ${JSON.stringify(ownApiVersions)}`)
const node = new DHT({ keyPair })
const server = node.createServer()
server.on('connection', function (socket: any) {
logger.info(`server on... with Remote public key: ${socket.remotePublicKey.toString('hex')}`)
socket.on('data', async (data: Buffer) => {
try {
if (data.length > 1141) {
logger.warn(
`received more than max allowed length of data buffer: ${data.length} against 1141 max allowed`,
)
return
}
logger.info(`data: ${data.toString('ascii')}`)
const recApiVersions: CommunityApi[] = JSON.parse(data.toString('ascii'))
// TODO better to introduce the validation by https://github.com/typestack/class-validato
if (recApiVersions && Array.isArray(recApiVersions) && recApiVersions.length < 5) {
for (const recApiVersion of recApiVersions) {
if (
!recApiVersion.api ||
typeof recApiVersion.api !== 'string' ||
!recApiVersion.url ||
typeof recApiVersion.url !== 'string'
) {
logger.warn(
`received invalid apiVersion-Definition: ${JSON.stringify(recApiVersion)}`,
)
// in a forEach-loop use return instead of continue
return
}
// TODO better to introduce the validation on entity-Level by https://github.com/typestack/class-validator
if (recApiVersion.api.length > 10 || recApiVersion.url.length > 255) {
logger.warn(
`received apiVersion with content longer than max length: ${JSON.stringify(
recApiVersion,
)}`,
)
// in a forEach-loop use return instead of continue
return
}
const variables = {
apiVersion: recApiVersion.api,
endPoint: recApiVersion.url,
publicKey: socket.remotePublicKey.toString('hex'),
lastAnnouncedAt: new Date(),
}
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()
.insert()
.into(DbCommunity)
.values(variables)
.orUpdate({
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
logger.info(`federation community upserted successfully...`)
}
} else {
logger.warn(
`received totaly wrong or too much apiVersions-Definition JSON-String: ${JSON.stringify(
recApiVersions,
)}`,
)
}
} catch (e) {
logger.error('Error on receiving data from socket:', e)
}
})
})
await server.listen()
setInterval(async () => {
logger.info(`Announcing on topic: ${TOPIC.toString('hex')}`)
await node.announce(TOPIC, keyPair).finished()
}, ANNOUNCETIME)
let successfulRequests: string[] = []
let errorfulRequests: string[] = []
setInterval(async () => {
logger.info('Refreshing successful nodes')
successfulRequests = []
}, SUCCESSTIME)
setInterval(async () => {
logger.info('Refreshing errorful nodes')
errorfulRequests = []
}, ERRORTIME)
setInterval(async () => {
const result = await node.lookup(TOPIC)
const collectedPubKeys: string[] = []
for await (const data of result) {
data.peers.forEach((peer: any) => {
const pubKey = peer.publicKey.toString('hex')
if (
pubKey !== keyPair.publicKey.toString('hex') &&
!successfulRequests.includes(pubKey) &&
!errorfulRequests.includes(pubKey) &&
!collectedPubKeys.includes(pubKey)
) {
collectedPubKeys.push(peer.publicKey.toString('hex'))
}
})
}
logger.info(`Found new peers: ${collectedPubKeys}`)
collectedPubKeys.forEach((remotePubKey) => {
const socket = node.connect(Buffer.from(remotePubKey, 'hex'))
// socket.once("connect", function () {
// console.log("client side emitted connect");
// });
// socket.once("end", function () {
// console.log("client side ended");
// });
socket.once('error', (err: any) => {
errorfulRequests.push(remotePubKey)
logger.error(`error on peer ${remotePubKey}: ${err.message}`)
})
socket.on('open', function () {
socket.write(Buffer.from(JSON.stringify(ownApiVersions)))
successfulRequests.push(remotePubKey)
})
})
}, POLLTIME)
} catch (err) {
logger.error('DHT unexpected error:', err)
}
}

View File

@ -0,0 +1,22 @@
import { Field, ObjectType, Query, Resolver } from 'type-graphql'
import { federationLogger as logger } from '@/server/logger'
@ObjectType()
class GetTestApiResult {
constructor(apiVersion: string) {
this.api = `${apiVersion}`
}
@Field(() => String)
api: string
}
@Resolver()
export class TestResolver {
@Query(() => GetTestApiResult)
async test(): Promise<GetTestApiResult> {
logger.info(`test api 1_0`)
return new GetTestApiResult("1_0")
}
}

View File

@ -0,0 +1,9 @@
import path from 'path'
// config
import CONFIG from '../../config'
import { federationLogger as logger } from '@/server/logger'
export const getApiResolvers = () => {
logger.info(`getApiResolvers...${CONFIG.FEDERATION_API}`)
return path.join(__dirname, `./${CONFIG.FEDERATION_API}/resolver/*Resolver.{ts,js}`)
}

View File

@ -0,0 +1,10 @@
import { Field, InputType } from 'type-graphql'
@InputType()
export default class SearchUsersFilters {
@Field(() => Boolean, { nullable: true, defaultValue: null })
byActivated: boolean
@Field(() => Boolean, { nullable: true, defaultValue: null })
byDeleted: boolean
}

View File

@ -0,0 +1,55 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { AuthChecker } from 'type-graphql'
import { decode, encode } from '@/auth/JWT'
import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN } from '@/auth/ROLES'
import { RIGHTS } from '@/auth/RIGHTS'
import { getCustomRepository } from '@dbTools/typeorm'
import { UserRepository } from '@repository/User'
import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS'
const isAuthorized: AuthChecker<any> = 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))
return true
// Do we have a token?
if (!context.token) {
throw new Error('401 Unauthorized')
}
// Decode the token
const decoded = decode(context.token)
if (!decoded) {
throw new Error('403.13 - Client certificate revoked')
}
// Set context pubKey
context.pubKey = Buffer.from(decoded.pubKey).toString('hex')
// TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests
// TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey
const userRepository = getCustomRepository(UserRepository)
try {
const user = await userRepository.findByPubkeyHex(context.pubKey)
context.user = user
context.role = user.isAdmin ? ROLE_ADMIN : ROLE_USER
} catch {
// in case the database query fails (user deleted)
throw new Error('401 Unauthorized')
}
// check for correct rights
const missingRights = (<RIGHTS[]>rights).filter((right) => !context.role.hasRight(right))
if (missingRights.length !== 0) {
throw new Error('401 Unauthorized')
}
// set new header token
context.setHeaders.push({ key: 'token', value: encode(decoded.pubKey) })
return true
}
export default isAuthorized

View File

@ -0,0 +1,23 @@
import { GraphQLScalarType, Kind } from 'graphql'
import Decimal from 'decimal.js-light'
export default new GraphQLScalarType({
name: 'Decimal',
description: 'The `Decimal` scalar type to represent currency values',
serialize(value: Decimal) {
return value.toString()
},
parseValue(value) {
return new Decimal(value)
},
parseLiteral(ast) {
if (ast.kind !== Kind.STRING) {
throw new TypeError(`${String(ast)} is not a valid decimal value.`)
}
return new Decimal(ast.value)
},
})

View File

@ -0,0 +1,17 @@
import { GraphQLSchema } from 'graphql'
import { buildSchema } from 'type-graphql'
import isAuthorized from './directive/isAuthorized'
import DecimalScalar from './scalar/Decimal'
import Decimal from 'decimal.js-light'
import { getApiResolvers } from './api/schema'
const schema = async (): Promise<GraphQLSchema> => {
return await buildSchema({
resolvers: [getApiResolvers()],
authChecker: isAuthorized,
scalarsMap: [{ type: Decimal, scalar: DecimalScalar }],
})
}
export default schema

40
dht-node/src/index.ts Normal file
View File

@ -0,0 +1,40 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import createServer from './server/createServer'
import { startDHT } from '@/dht_node/index'
// config
import CONFIG from './config'
async function main() {
// eslint-disable-next-line no-console
console.log(`FEDERATION_PORT=${CONFIG.FEDERATION_PORT}`)
console.log(`FEDERATION_API=${CONFIG.FEDERATION_API}`)
console.log(`configured: FEDERATION_DHT_TOPIC=${CONFIG.FEDERATION_DHT_TOPIC}`)
const { app } = await createServer()
app.listen(CONFIG.FEDERATION_PORT, () => {
// eslint-disable-next-line no-console
console.log(`Server is running at http://localhost:${CONFIG.FEDERATION_PORT}`)
if (CONFIG.GRAPHIQL) {
// eslint-disable-next-line no-console
console.log(`GraphIQL available at http://localhost:${CONFIG.FEDERATION_PORT}`)
}
})
// start DHT hyperswarm when DHT_TOPIC is set in .env
if (CONFIG.FEDERATION_DHT_TOPIC) {
// eslint-disable-next-line no-console
console.log(
`starting Federation on ${CONFIG.FEDERATION_DHT_TOPIC} ${
CONFIG.FEDERATION_DHT_SEED ? 'with seed...' : 'without seed...'
}`,
)
await startDHT(CONFIG.FEDERATION_DHT_TOPIC) // con,
}
}
main().catch((e) => {
// eslint-disable-next-line no-console
console.error(e)
process.exit(1)
})

View File

@ -0,0 +1,51 @@
import { Role } from '@/auth/Role'
import { User as dbUser } from '@entity/User'
import { Transaction as dbTransaction } from '@entity/Transaction'
import Decimal from 'decimal.js-light'
import { ExpressContext } from 'apollo-server-express'
export interface Context {
token: string | null
setHeaders: { key: string; value: string }[]
role?: Role
user?: dbUser
clientTimezoneOffset?: number
// hack to use less DB calls for Balance Resolver
lastTransaction?: dbTransaction
transactionCount?: number
linkCount?: number
sumHoldAvailableAmount?: Decimal
}
const context = (args: ExpressContext): Context => {
const authorization = args.req.headers.authorization
const clientTimezoneOffset = args.req.headers.clienttimezoneoffset
const context: Context = {
token: null,
setHeaders: [],
}
if (authorization) {
context.token = authorization.replace(/^Bearer /, '')
}
if (clientTimezoneOffset && typeof clientTimezoneOffset === 'string') {
context.clientTimezoneOffset = Number(clientTimezoneOffset)
}
return context
}
export const getUser = (context: Context): dbUser => {
if (context.user) return context.user
throw new Error('No user given in context!')
}
export const getClientTimezoneOffset = (context: Context): number => {
if (
(context.clientTimezoneOffset || context.clientTimezoneOffset === 0) &&
Math.abs(context.clientTimezoneOffset) <= 27 * 60
) {
return context.clientTimezoneOffset
}
throw new Error('No valid client time zone offset in context!')
}
export default context

View File

@ -0,0 +1,8 @@
import cors from 'cors'
const corsOptions = {
origin: '*',
exposedHeaders: ['token'],
}
export default cors(corsOptions)

View File

@ -0,0 +1,91 @@
import 'reflect-metadata'
import { ApolloServer } from 'apollo-server-express'
import express, { Express } from 'express'
// database
import connection from '@/typeorm/connection'
import { checkDBVersion } from '@/typeorm/DBVersion'
// server
import cors from './cors'
// import serverContext from './context'
// import plugins from './plugins'
// config
import CONFIG from '@/config'
// graphql
import schema from '@/graphql/schema'
// webhooks
// import { elopageWebhook } from '@/webhook/elopage'
import { Connection } from '@dbTools/typeorm'
import { apolloLogger } from './logger'
import { Logger } from 'log4js'
// i18n
import { i18n } from './localization'
// TODO implement
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
type ServerDef = { apollo: ApolloServer; app: Express; con: Connection }
const createServer = async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// context: any = serverContext,
logger: Logger = apolloLogger,
localization: i18n.I18n = i18n,
): Promise<ServerDef> => {
logger.addContext('user', 'unknown')
logger.debug('createServer...')
// open mysql connection
const con = await connection()
if (!con || !con.isConnected) {
logger.fatal(`Couldn't open connection to database!`)
throw new Error(`Fatal: Couldn't open connection to database`)
}
// check for correct database version
const dbVersion = await checkDBVersion(CONFIG.DB_VERSION)
if (!dbVersion) {
logger.fatal('Fatal: Database Version incorrect')
throw new Error('Fatal: Database Version incorrect')
}
// Express Server
const app = express()
// cors
app.use(cors)
// bodyparser json
app.use(express.json())
// bodyparser urlencoded for elopage
app.use(express.urlencoded({ extended: true }))
// i18n
app.use(localization.init)
// Elopage Webhook
// app.post('/hook/elopage/' + CONFIG.WEBHOOK_ELOPAGE_SECRET, elopageWebhook)
// Apollo Server
const apollo = new ApolloServer({
schema: await schema(),
// playground: CONFIG.GRAPHIQL,
// introspection: CONFIG.GRAPHIQL,
// context,
// plugins,
logger,
})
apollo.applyMiddleware({ app, path: '/' })
logger.debug('createServer...successful')
return { apollo, app, con }
}
export default createServer

View File

@ -0,0 +1,28 @@
import path from 'path'
import { federationLogger as logger } from './logger'
import i18n from 'i18n'
i18n.configure({
locales: ['en', 'de'],
defaultLocale: 'en',
retryInDefaultLocale: false,
directory: path.join(__dirname, '..', 'locales'),
// autoReload: true, // if this is activated the seeding hangs at the very end
updateFiles: false,
objectNotation: true,
logDebugFn: (msg) => logger.debug(msg),
logWarnFn: (msg) => logger.info(msg),
logErrorFn: (msg) => logger.error(msg),
// this api is needed for email-template pug files
api: {
__: 't', // now req.__ becomes req.t
__n: 'tn', // and req.__n can be called as req.tn
},
register: global,
mustacheConfig: {
tags: ['{', '}'],
disable: false,
},
})
export { i18n }

View File

@ -0,0 +1,33 @@
import log4js from 'log4js'
import CONFIG from '@/config'
import { readFileSync } from 'fs'
const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8'))
options.categories.backend.level = CONFIG.LOG_LEVEL
options.categories.apollo.level = CONFIG.LOG_LEVEL
let filename: string = options.appenders.federation.filename
if(CONFIG.FEDERATION_DHT_TOPIC) {
options.appenders.federation.filename = filename.replace('apiversion-%v', 'dht-'+CONFIG.FEDERATION_DHT_TOPIC).replace('%p', CONFIG.FEDERATION_PORT.toString())
} else {
options.appenders.federation.filename = filename.replace('%v', CONFIG.FEDERATION_API).replace('%p', CONFIG.FEDERATION_PORT.toString())
}
filename = options.appenders.access.filename
options.appenders.access.filename = filename.replace('%p', CONFIG.FEDERATION_PORT.toString())
filename = options.appenders.apollo.filename
options.appenders.apollo.filename = filename.replace('%p', CONFIG.FEDERATION_PORT.toString())
filename = options.appenders.backend.filename
options.appenders.backend.filename = filename.replace('%p', CONFIG.FEDERATION_PORT.toString())
filename = options.appenders.errorFile.filename
options.appenders.errorFile.filename = filename.replace('%p', CONFIG.FEDERATION_PORT.toString())
log4js.configure(options)
const apolloLogger = log4js.getLogger('apollo')
// const backendLogger = log4js.getLogger('backend')
const federationLogger = log4js.getLogger('federation')
// backendLogger.addContext('user', 'unknown')
export { apolloLogger, federationLogger }

View File

@ -0,0 +1,61 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import clonedeep from 'lodash.clonedeep'
const setHeadersPlugin = {
requestDidStart() {
return {
willSendResponse(requestContext: any) {
const { setHeaders = [] } = requestContext.context
setHeaders.forEach(({ key, value }: { [key: string]: string }) => {
if (requestContext.response.http.headers.get(key)) {
requestContext.response.http.headers.set(key, value)
} else {
requestContext.response.http.headers.append(key, value)
}
})
return requestContext
},
}
},
}
const filterVariables = (variables: any) => {
const vars = clonedeep(variables)
if (vars.password) vars.password = '***'
if (vars.passwordNew) vars.passwordNew = '***'
return vars
}
const logPlugin = {
requestDidStart(requestContext: any) {
const { logger } = requestContext
const { query, mutation, variables, operationName } = requestContext.request
if (operationName !== 'IntrospectionQuery') {
logger.info(`Request:
${mutation || query}variables: ${JSON.stringify(filterVariables(variables), null, 2)}`)
}
return {
willSendResponse(requestContext: any) {
if (operationName !== 'IntrospectionQuery') {
if (requestContext.context.user) logger.info(`User ID: ${requestContext.context.user.id}`)
if (requestContext.response.data) {
logger.info('Response Success!')
logger.trace(`Response-Data:
${JSON.stringify(requestContext.response.data, null, 2)}`)
}
if (requestContext.response.errors)
logger.error(`Response-Errors:
${JSON.stringify(requestContext.response.errors, null, 2)}`)
}
return requestContext
},
}
},
}
const plugins =
process.env.NODE_ENV === 'development' ? [setHeadersPlugin] : [setHeadersPlugin, logPlugin]
export default plugins

View File

@ -0,0 +1,27 @@
import { Migration } from '@entity/Migration'
import { federationLogger as logger } from '@/server/logger'
const getDBVersion = async (): Promise<string | null> => {
try {
const dbVersion = await Migration.findOne({ order: { version: 'DESC' } })
return dbVersion ? dbVersion.fileName : null
} catch (error) {
logger.error(error)
return null
}
}
const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => {
const dbVersion = await getDBVersion()
if (!dbVersion || dbVersion.indexOf(DB_VERSION) === -1) {
logger.error(
`Wrong database version detected - the backend requires '${DB_VERSION}' but found '${
dbVersion || 'None'
}`,
)
return false
}
return true
}
export { checkDBVersion, getDBVersion }

View File

@ -0,0 +1,34 @@
// TODO This is super weird - since the entities are defined in another project they have their own globals.
// We cannot use our connection here, but must use the external typeorm installation
import { Connection, createConnection, FileLogger } from '@dbTools/typeorm'
import CONFIG from '@/config'
import { entities } from '@entity/index'
const connection = async (): Promise<Connection | null> => {
try {
return createConnection({
name: 'default',
type: 'mysql',
host: CONFIG.DB_HOST,
port: CONFIG.DB_PORT,
username: CONFIG.DB_USER,
password: CONFIG.DB_PASSWORD,
database: CONFIG.DB_DATABASE,
entities,
synchronize: false,
logging: true,
logger: new FileLogger('all', {
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
}),
extra: {
charset: 'utf8mb4_unicode_ci',
},
})
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)
return null
}
}
export default connection

View File

@ -0,0 +1,67 @@
import SearchUsersFilters from '@/graphql/arg/SearchUsersFilters'
import { Brackets, EntityRepository, IsNull, Not, Repository } from '@dbTools/typeorm'
import { User as DbUser } from '@entity/User'
@EntityRepository(DbUser)
export class UserRepository extends Repository<DbUser> {
async findByPubkeyHex(pubkeyHex: string): Promise<DbUser> {
const dbUser = await this.createQueryBuilder('user')
.leftJoinAndSelect('user.emailContact', 'emailContact')
.where('hex(user.pubKey) = :pubkeyHex', { pubkeyHex })
.getOneOrFail()
/*
const dbUser = await this.findOneOrFail(`hex(user.pubKey) = { pubkeyHex }`)
const emailContact = await this.query(
`SELECT * from user_contacts where id = { dbUser.emailId }`,
)
dbUser.emailContact = emailContact
*/
return dbUser
}
async findBySearchCriteriaPagedFiltered(
select: string[],
searchCriteria: string,
filters: SearchUsersFilters,
currentPage: number,
pageSize: number,
): Promise<[DbUser[], number]> {
const query = this.createQueryBuilder('user')
.select(select)
.withDeleted()
.leftJoinAndSelect('user.emailContact', 'emailContact')
.where(
new Brackets((qb) => {
qb.where(
'user.firstName like :name or user.lastName like :lastName or emailContact.email like :email',
{
name: `%${searchCriteria}%`,
lastName: `%${searchCriteria}%`,
email: `%${searchCriteria}%`,
},
)
}),
)
/*
filterCriteria.forEach((filter) => {
query.andWhere(filter)
})
*/
if (filters) {
if (filters.byActivated !== null) {
query.andWhere('emailContact.emailChecked = :value', { value: filters.byActivated })
// filterCriteria.push({ 'emailContact.emailChecked': filters.byActivated })
}
if (filters.byDeleted !== null) {
// filterCriteria.push({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() })
query.andWhere({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() })
}
}
return query
.take(pageSize)
.skip((currentPage - 1) * pageSize)
.getManyAndCount()
}
}

90
dht-node/tsconfig.json Normal file
View File

@ -0,0 +1,90 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./build", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
"strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": ".", /* Base directory to resolve non-absolute module names. */
"paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
"@/*": ["src/*"],
// "@arg/*": ["src/graphql/arg/*"],
// "@enum/*": ["src/graphql/enum/*"],
// "@model/*": ["src/graphql/model/*"],
"@repository/*": ["src/typeorm/repository/*"],
// "@test/*": ["test/*"],
/* external */
"@typeorm/*": ["../backend/src/typeorm/*", "../../backend/src/typeorm/*"],
"@dbTools/*": ["../database/src/*", "../../database/build/src/*"],
"@entity/*": ["../database/entity/*", "../../database/build/entity/*"]
},
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": ["src/dht_node/@types", "node_modules/@types"], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"references": [
{
"path": "../database/tsconfig.json",
// add 'prepend' if you want to include the referenced project in your output file
// "prepend": true
}
]
}

2748
dht-node/yarn.lock Normal file

File diff suppressed because it is too large Load Diff