Merge branch 'master' into refactor-federation-use-inheritance

This commit is contained in:
Ulf Gebhardt 2023-06-06 11:01:14 +02:00
commit 9f0043e4ef
Signed by: ulfgebhardt
GPG Key ID: DA6B843E748679C9
25 changed files with 428 additions and 80 deletions

View File

@ -1,10 +1,11 @@
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { GraphQLClient } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import { getPublicKey } from '@/federation/query/getPublicKey' import { getPublicKey } from '@/federation/client/1_0/query/getPublicKey'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
export class FederationClient_1_0 { // eslint-disable-next-line camelcase
export class FederationClient {
dbCom: DbFederatedCommunity dbCom: DbFederatedCommunity
endpoint: string endpoint: string
client: GraphQLClient client: GraphQLClient

View File

@ -0,0 +1,5 @@
// eslint-disable-next-line camelcase
import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient'
// eslint-disable-next-line camelcase
export class FederationClient extends V1_0_FederationClient {}

View File

@ -1,21 +1,23 @@
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
// eslint-disable-next-line camelcase
import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient'
// eslint-disable-next-line camelcase
import { FederationClient as V1_1_FederationClient } from '@/federation/client/1_1/FederationClient'
import { ApiVersionType } from '@/federation/enum/apiVersionType' import { ApiVersionType } from '@/federation/enum/apiVersionType'
import { FederationClient_1_0 } from './FederationClient_1_0' // eslint-disable-next-line camelcase
import { FederationClient_1_1 } from './FederationClient_1_1' type FederationClient = V1_0_FederationClient | V1_1_FederationClient
type FederationClientType = FederationClient_1_0 | FederationClient_1_1 interface FederationClientInstance {
interface ClientInstance {
id: number id: number
// eslint-disable-next-line no-use-before-define // eslint-disable-next-line no-use-before-define
client: FederationClientType client: FederationClient
} }
// eslint-disable-next-line @typescript-eslint/no-extraneous-class // eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class FederationClient { export class FederationClientFactory {
private static instanceArray: ClientInstance[] = [] private static instanceArray: FederationClientInstance[] = []
/** /**
* The Singleton's constructor should always be private to prevent direct * The Singleton's constructor should always be private to prevent direct
@ -27,9 +29,9 @@ export class FederationClient {
private static createFederationClient = (dbCom: DbFederatedCommunity) => { private static createFederationClient = (dbCom: DbFederatedCommunity) => {
switch (dbCom.apiVersion) { switch (dbCom.apiVersion) {
case ApiVersionType.V1_0: case ApiVersionType.V1_0:
return new FederationClient_1_0(dbCom) return new V1_0_FederationClient(dbCom)
case ApiVersionType.V1_1: case ApiVersionType.V1_1:
return new FederationClient_1_1(dbCom) return new V1_1_FederationClient(dbCom)
default: default:
return null return null
} }
@ -41,14 +43,19 @@ export class FederationClient {
* This implementation let you subclass the Singleton class while keeping * This implementation let you subclass the Singleton class while keeping
* just one instance of each subclass around. * just one instance of each subclass around.
*/ */
public static getInstance(dbCom: DbFederatedCommunity): FederationClientType | null { public static getInstance(dbCom: DbFederatedCommunity): FederationClient | null {
const instance = FederationClient.instanceArray.find((instance) => instance.id === dbCom.id) const instance = FederationClientFactory.instanceArray.find(
(instance) => instance.id === dbCom.id,
)
if (instance) { if (instance) {
return instance.client return instance.client
} }
const client = FederationClient.createFederationClient(dbCom) const client = FederationClientFactory.createFederationClient(dbCom)
if (client) { if (client) {
FederationClient.instanceArray.push({ id: dbCom.id, client } as ClientInstance) FederationClientFactory.instanceArray.push({
id: dbCom.id,
client,
} as FederationClientInstance)
} }
return client return client
} }

View File

@ -1,3 +0,0 @@
import { FederationClient_1_0 } from './FederationClient_1_0'
export class FederationClient_1_1 extends FederationClient_1_0 {}

View File

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

View File

@ -3,9 +3,11 @@
import { IsNull } from '@dbTools/typeorm' import { IsNull } from '@dbTools/typeorm'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
// eslint-disable-next-line camelcase
import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient'
import { FederationClientFactory } from '@/federation/client/FederationClientFactory'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
import { FederationClient } from './client/FederationClient'
import { ApiVersionType } from './enum/apiVersionType' import { ApiVersionType } from './enum/apiVersionType'
export function startValidateCommunities(timerInterval: number): void { export function startValidateCommunities(timerInterval: number): void {
@ -37,11 +39,13 @@ export async function validateCommunities(): Promise<void> {
continue continue
} }
try { try {
const client = FederationClient.getInstance(dbCom) const client = FederationClientFactory.getInstance(dbCom)
const pubKey = await client?.getPublicKey() // eslint-disable-next-line camelcase
if (client instanceof V1_0_FederationClient) {
const pubKey = await client.getPublicKey()
if (pubKey && pubKey === dbCom.publicKey.toString()) { if (pubKey && pubKey === dbCom.publicKey.toString()) {
await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() }) await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() })
logger.info('Federation: verified community', dbCom) logger.info('Federation: verified community with', dbCom.endPoint)
} else { } else {
logger.warn( logger.warn(
'Federation: received not matching publicKey:', 'Federation: received not matching publicKey:',
@ -49,6 +53,7 @@ export async function validateCommunities(): Promise<void> {
dbCom.publicKey.toString(), dbCom.publicKey.toString(),
) )
} }
}
} catch (err) { } catch (err) {
logger.error(`Error:`, err) logger.error(`Error:`, err)
} }

View File

@ -322,8 +322,6 @@ export class TransactionResolver {
throw new LogError('Amount to send must be positive', amount) throw new LogError('Amount to send must be positive', amount)
} }
// TODO this is subject to replay attacks
// --- WHY?
const senderUser = getUser(context) const senderUser = getUser(context)
// validate recipient user // validate recipient user

View File

@ -66,8 +66,6 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis
) )
} }
/* 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>>) { 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_gradido_id`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `user_name`;') await queryFn('ALTER TABLE `transactions` DROP COLUMN `user_name`;')

View File

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

View File

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

View File

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

View File

@ -23,7 +23,8 @@
"nodemon": "^2.0.20", "nodemon": "^2.0.20",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.2", "tsconfig-paths": "^4.1.2",
"typescript": "^4.9.4" "typescript": "^4.9.4",
"uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@types/dotenv": "^8.2.0", "@types/dotenv": "^8.2.0",
@ -31,6 +32,7 @@
"@types/node": "^18.11.18", "@types/node": "^18.11.18",
"@typescript-eslint/eslint-plugin": "^5.48.0", "@typescript-eslint/eslint-plugin": "^5.48.0",
"@typescript-eslint/parser": "^5.48.0", "@typescript-eslint/parser": "^5.48.0",
"@types/uuid": "^8.3.4",
"eslint": "^8.31.0", "eslint": "^8.31.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^17.0.0", "eslint-config-standard": "^17.0.0",

View File

@ -9,7 +9,7 @@ const constants = {
LOG_LEVEL: process.env.LOG_LEVEL || 'info', LOG_LEVEL: process.env.LOG_LEVEL || 'info',
CONFIG_VERSION: { CONFIG_VERSION: {
DEFAULT: 'DEFAULT', DEFAULT: 'DEFAULT',
EXPECTED: 'v2.2023-02-07', EXPECTED: 'v3.2023-04-26',
CURRENT: '', CURRENT: '',
}, },
} }
@ -28,6 +28,12 @@ const database = {
process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.dht-node.log', process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.dht-node.log',
} }
const community = {
COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung',
COMMUNITY_DESCRIPTION:
process.env.COMMUNITY_DESCRIPTION || 'Gradido-Community einer lokalen Entwicklungsumgebung.',
}
const federation = { const federation = {
FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || 'GRADIDO_HUB', FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || 'GRADIDO_HUB',
FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null, FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
@ -51,6 +57,7 @@ const CONFIG = {
...constants, ...constants,
...server, ...server,
...database, ...database,
...community,
...federation, ...federation,
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -769,6 +769,11 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
"@types/uuid@^8.3.4":
version "8.3.4"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
"@types/yargs-parser@*": "@types/yargs-parser@*":
version "21.0.0" version "21.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
@ -4138,6 +4143,11 @@ url-parse@^1.5.3:
querystringify "^2.1.1" querystringify "^2.1.1"
requires-port "^1.0.0" requires-port "^1.0.0"
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
v8-compile-cache-lib@^3.0.1: v8-compile-cache-lib@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"

View File

@ -1,11 +1,12 @@
<template> <template>
<div class="redeem-information"> <div class="redeem-information">
<b-jumbotron bg-variant="muted" text-variant="dark" border-variant="info"> <b-jumbotron bg-variant="muted" text-variant="dark" border-variant="info">
<h1 v-if="isContributionLink"> <h1 v-if="amount === ''">{{ $t('gdd_per_link.redeemlink-error') }}</h1>
<h1 v-if="isContributionLink && amount !== ''">
{{ CONFIG.COMMUNITY_NAME }} {{ CONFIG.COMMUNITY_NAME }}
{{ $t('contribution-link.thanksYouWith') }} {{ amount | GDD }} {{ $t('contribution-link.thanksYouWith') }} {{ amount | GDD }}
</h1> </h1>
<h1 v-else> <h1 v-if="!isContributionLink && amount !== ''">
{{ user.firstName }} {{ user.firstName }}
{{ $t('transaction-link.send_you') }} {{ amount | GDD }} {{ $t('transaction-link.send_you') }} {{ amount | GDD }}
</h1> </h1>

View File

@ -3,7 +3,12 @@
<redeem-information v-bind="linkData" :isContributionLink="isContributionLink" /> <redeem-information v-bind="linkData" :isContributionLink="isContributionLink" />
<b-jumbotron> <b-jumbotron>
<div class="mb-3 text-center"> <div class="mb-3 text-center">
<b-button variant="gradido" @click="$emit('mutation-link', linkData.amount)" size="lg"> <b-button
variant="gradido"
@click="$emit('mutation-link', linkData.amount)"
size="lg"
:disabled="!validLink"
>
{{ $t('gdd_per_link.redeem') }} {{ $t('gdd_per_link.redeem') }}
</b-button> </b-button>
</div> </div>
@ -21,6 +26,7 @@ export default {
props: { props: {
linkData: { type: Object, required: true }, linkData: { type: Object, required: true },
isContributionLink: { type: Boolean, default: false }, isContributionLink: { type: Boolean, default: false },
validLink: { type: Boolean, default: false },
}, },
} }
</script> </script>

View File

@ -210,6 +210,7 @@
"redeemed": "Erfolgreich eingelöst! Deinem Konto wurden {n} GDD gutgeschrieben.", "redeemed": "Erfolgreich eingelöst! Deinem Konto wurden {n} GDD gutgeschrieben.",
"redeemed-at": "Der Link wurde bereits am {date} eingelöst.", "redeemed-at": "Der Link wurde bereits am {date} eingelöst.",
"redeemed-title": "eingelöst", "redeemed-title": "eingelöst",
"redeemlink-error": "Dieser Einlöse-Link ist nicht vollständig.",
"to-login": "Log dich ein", "to-login": "Log dich ein",
"to-register": "Registriere ein neues Konto.", "to-register": "Registriere ein neues Konto.",
"validUntil": "Gültig bis", "validUntil": "Gültig bis",

View File

@ -210,6 +210,7 @@
"redeemed": "Successfully redeemed! Your account has been credited with {n} GDD.", "redeemed": "Successfully redeemed! Your account has been credited with {n} GDD.",
"redeemed-at": "The link was already redeemed on {date}.", "redeemed-at": "The link was already redeemed on {date}.",
"redeemed-title": "redeemed", "redeemed-title": "redeemed",
"redeemlink-error": "This redemption link is not complete.",
"to-login": "Log in", "to-login": "Log in",
"to-register": "Register a new account.", "to-register": "Register a new account.",
"validUntil": "Valid until", "validUntil": "Valid until",

View File

@ -374,12 +374,12 @@ describe('TransactionLink', () => {
describe('error on transaction link query', () => { describe('error on transaction link query', () => {
beforeEach(() => { beforeEach(() => {
apolloQueryMock.mockRejectedValue({ message: 'Ouchh!' }) apolloQueryMock.mockRejectedValue({ message: 'gdd_per_link.redeemlink-error' })
wrapper = Wrapper() wrapper = Wrapper()
}) })
it('toasts an error message', () => { it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Ouchh!') expect(toastErrorSpy).toBeCalledWith('gdd_per_link.redeemlink-error')
}) })
}) })
}) })

View File

@ -14,6 +14,7 @@
<redeem-valid <redeem-valid
:linkData="linkData" :linkData="linkData"
:isContributionLink="isContributionLink" :isContributionLink="isContributionLink"
:validLink="validLink"
@mutation-link="mutationLink" @mutation-link="mutationLink"
/> />
</template> </template>
@ -47,12 +48,13 @@ export default {
return { return {
linkData: { linkData: {
__typename: 'TransactionLink', __typename: 'TransactionLink',
amount: '123.45', amount: '',
memo: 'memo', memo: '',
user: { user: {
firstName: 'Bibi', firstName: '',
}, },
deletedAt: null, deletedAt: null,
validLink: false,
}, },
} }
}, },
@ -67,13 +69,14 @@ export default {
}, },
}) })
.then((result) => { .then((result) => {
this.validLink = true
this.linkData = result.data.queryTransactionLink this.linkData = result.data.queryTransactionLink
if (this.linkData.__typename === 'ContributionLink' && this.$store.state.token) { if (this.linkData.__typename === 'ContributionLink' && this.$store.state.token) {
this.mutationLink(this.linkData.amount) this.mutationLink(this.linkData.amount)
} }
}) })
.catch((err) => { .catch(() => {
this.toastError(err.message) this.toastError(this.$t('gdd_per_link.redeemlink-error'))
}) })
}, },
mutationLink(amount) { mutationLink(amount) {