mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge branch 'release-2_7_0' into dlt_connector_direct_usage
This commit is contained in:
commit
1230799a2e
1
.bun-version
Normal file
1
.bun-version
Normal file
@ -0,0 +1 @@
|
||||
1.3.0
|
||||
2
.github/workflows/test_admin_interface.yml
vendored
2
.github/workflows/test_admin_interface.yml
vendored
@ -55,7 +55,7 @@ jobs:
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.2.23
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: install dependencies
|
||||
run: |
|
||||
|
||||
4
.github/workflows/test_backend.yml
vendored
4
.github/workflows/test_backend.yml
vendored
@ -56,6 +56,8 @@ jobs:
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: install dependencies
|
||||
run: |
|
||||
@ -81,6 +83,8 @@ jobs:
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: install dependencies
|
||||
run: |
|
||||
|
||||
2
.github/workflows/test_config.yml
vendored
2
.github/workflows/test_config.yml
vendored
@ -31,6 +31,8 @@ jobs:
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: install dependencies
|
||||
run: bun install --filter config-schema --frozen-lockfile
|
||||
|
||||
2
.github/workflows/test_core.yml
vendored
2
.github/workflows/test_core.yml
vendored
@ -32,6 +32,8 @@ jobs:
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: install dependencies
|
||||
run: |
|
||||
|
||||
6
.github/workflows/test_database.yml
vendored
6
.github/workflows/test_database.yml
vendored
@ -53,6 +53,8 @@ jobs:
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: install dependencies
|
||||
run: |
|
||||
@ -76,7 +78,9 @@ jobs:
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: install dependencies
|
||||
run: |
|
||||
bun install --filter database --frozen-lockfile
|
||||
|
||||
2
.github/workflows/test_dht_node.yml
vendored
2
.github/workflows/test_dht_node.yml
vendored
@ -53,6 +53,8 @@ jobs:
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: install dependencies
|
||||
run: |
|
||||
|
||||
6
.github/workflows/test_e2e.yml
vendored
6
.github/workflows/test_e2e.yml
vendored
@ -17,6 +17,8 @@ jobs:
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: Boot up test system | docker-compose mariadb mailserver
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb mailserver
|
||||
@ -120,6 +122,8 @@ jobs:
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: Boot up test system | docker-compose mariadb mailserver
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb mailserver
|
||||
@ -203,6 +207,8 @@ jobs:
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: Boot up test system | docker-compose mariadb mailserver
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb mailserver
|
||||
|
||||
2
.github/workflows/test_federation.yml
vendored
2
.github/workflows/test_federation.yml
vendored
@ -53,6 +53,8 @@ jobs:
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: install dependencies
|
||||
run: |
|
||||
|
||||
6
.github/workflows/test_frontend.yml
vendored
6
.github/workflows/test_frontend.yml
vendored
@ -52,6 +52,8 @@ jobs:
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: install dependencies
|
||||
run: bun install --filter frontend --frozen-lockfile
|
||||
@ -77,7 +79,9 @@ jobs:
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: install dependencies
|
||||
run: |
|
||||
bun install --filter frontend --frozen-lockfile
|
||||
|
||||
2
.github/workflows/test_shared.yml
vendored
2
.github/workflows/test_shared.yml
vendored
@ -30,6 +30,8 @@ jobs:
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: install dependencies
|
||||
run: bun install --filter shared --frozen-lockfile
|
||||
|
||||
33
CHANGELOG.md
33
CHANGELOG.md
@ -4,8 +4,41 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [v2.7.0](https://github.com/gradido/gradido/compare/v2.7.0...v2.7.0)
|
||||
|
||||
- fixes [`414ff8a`](https://github.com/gradido/gradido/commit/414ff8ac5a7477109f80123ccca5c4c8ed4511b2)
|
||||
|
||||
#### [v2.7.0](https://github.com/gradido/gradido/compare/2.6.1...v2.7.0)
|
||||
|
||||
> 15 October 2025
|
||||
|
||||
- feat(frontend): gradido id under avatar instead of email [`#3543`](https://github.com/gradido/gradido/pull/3543)
|
||||
- refactor(backend): add and use template function updateAllDefinedAndChanged in update user and community [`#3546`](https://github.com/gradido/gradido/pull/3546)
|
||||
- fix(backend): check for openai thread timeout [`#3549`](https://github.com/gradido/gradido/pull/3549)
|
||||
- feat(frontend): paste community and user in recipient field of gdd send dialog [`#3542`](https://github.com/gradido/gradido/pull/3542)
|
||||
- fix(backend): allow reading gmsApiKey admins only [`#3547`](https://github.com/gradido/gradido/pull/3547)
|
||||
- feat(frontend): make link send confirmation dialog more accurate [`#3548`](https://github.com/gradido/gradido/pull/3548)
|
||||
- fix(federation): fix bug [`#3545`](https://github.com/gradido/gradido/pull/3545)
|
||||
- feat(frontend): modify frontend for cross community redeem link disbursement [`#3537`](https://github.com/gradido/gradido/pull/3537)
|
||||
- fix(frontend): decimal comma to point on contribution form [`#3539`](https://github.com/gradido/gradido/pull/3539)
|
||||
- feat(backend): introduce security in disbursement handshake [`#3523`](https://github.com/gradido/gradido/pull/3523)
|
||||
- feat(frontend): update texts for overview and contribution message [`#3532`](https://github.com/gradido/gradido/pull/3532)
|
||||
- feat(frontend): add link to gdd in overview [`#3531`](https://github.com/gradido/gradido/pull/3531)
|
||||
- feat(other): add infos for using logging in tests [`#3530`](https://github.com/gradido/gradido/pull/3530)
|
||||
- feat(frontend): increase memo [`#3527`](https://github.com/gradido/gradido/pull/3527)
|
||||
- feat(admin): add hiero topic id like gms api key [`#3524`](https://github.com/gradido/gradido/pull/3524)
|
||||
- fix(other): publish only on release [`#3529`](https://github.com/gradido/gradido/pull/3529)
|
||||
|
||||
#### [2.6.1](https://github.com/gradido/gradido/compare/2.3.1...2.6.1)
|
||||
|
||||
> 14 August 2025
|
||||
|
||||
- fix(other): remove mariadb from publish workflow [`#3528`](https://github.com/gradido/gradido/pull/3528)
|
||||
- fix(database): docker setup [`#3526`](https://github.com/gradido/gradido/pull/3526)
|
||||
- fix(other): fix problems with bun and e2e [`#3525`](https://github.com/gradido/gradido/pull/3525)
|
||||
- feat(backend): introduce security in x com tx handshake [`#3520`](https://github.com/gradido/gradido/pull/3520)
|
||||
- feat(backend): openid connect routes [`#3518`](https://github.com/gradido/gradido/pull/3518)
|
||||
- chore(release): v2.6.1 beta [`#3521`](https://github.com/gradido/gradido/pull/3521)
|
||||
- refactor(frontend): transaction and contribution form [`#3519`](https://github.com/gradido/gradido/pull/3519)
|
||||
- fix(federation): fix some attack vectors in communities handshake [`#3517`](https://github.com/gradido/gradido/pull/3517)
|
||||
- fix(other): start sh when called from webhook [`#3515`](https://github.com/gradido/gradido/pull/3515)
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"description": "Administration Interface for Gradido",
|
||||
"main": "index.js",
|
||||
"author": "Gradido Academy - https://www.gradido.net",
|
||||
"version": "2.6.1",
|
||||
"version": "2.7.0",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@ -61,6 +61,7 @@
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"config-schema": "*",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^10.0.0",
|
||||
"dotenv-webpack": "^7.0.3",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "2.6.1",
|
||||
"version": "2.7.0",
|
||||
"private": false,
|
||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||
"repository": "https://github.com/gradido/gradido/backend",
|
||||
@ -41,6 +41,7 @@
|
||||
"@swc/cli": "^0.7.3",
|
||||
"@swc/core": "^1.11.24",
|
||||
"@swc/helpers": "^0.5.17",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/email-templates": "^10.0.4",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/faker": "^5.5.9",
|
||||
|
||||
@ -1,78 +1,110 @@
|
||||
import { CommunityLoggingView, Community as DbCommunity, FederatedCommunity as DbFederatedCommunity, FederatedCommunityLoggingView, getHomeCommunity } from 'database'
|
||||
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
||||
import {
|
||||
CommunityHandshakeState as DbCommunityHandshakeState,
|
||||
CommunityHandshakeStateLoggingView,
|
||||
FederatedCommunity as DbFederatedCommunity,
|
||||
findPendingCommunityHandshake,
|
||||
getHomeCommunityWithFederatedCommunityOrFail,
|
||||
CommunityHandshakeStateType,
|
||||
getCommunityByPublicKeyOrFail,
|
||||
} from 'database'
|
||||
import { randombytes_random } from 'sodium-native'
|
||||
import { CONFIG as CONFIG_CORE } from 'core'
|
||||
|
||||
import { AuthenticationClient as V1_0_AuthenticationClient } from '@/federation/client/1_0/AuthenticationClient'
|
||||
import { ensureUrlEndsWithSlash } from 'core'
|
||||
import { ensureUrlEndsWithSlash, getFederatedCommunityWithApiOrFail } from 'core'
|
||||
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { encryptAndSign, OpenConnectionJwtPayloadType } from 'shared'
|
||||
import { communityAuthenticatedSchema, encryptAndSign, OpenConnectionJwtPayloadType } from 'shared'
|
||||
import { getLogger } from 'log4js'
|
||||
import { AuthenticationClientFactory } from './client/AuthenticationClientFactory'
|
||||
import { EncryptedTransferArgs } from 'core'
|
||||
import { CommunityHandshakeStateLogic } from 'core'
|
||||
import { Ed25519PublicKey } from 'shared'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities`)
|
||||
const createLogger = (functionName: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities.${functionName}`)
|
||||
|
||||
export enum StartCommunityAuthenticationResult {
|
||||
ALREADY_AUTHENTICATED = 'already authenticated',
|
||||
ALREADY_IN_PROGRESS = 'already in progress',
|
||||
SUCCESSFULLY_STARTED = 'successfully started',
|
||||
}
|
||||
|
||||
export async function startCommunityAuthentication(
|
||||
fedComB: DbFederatedCommunity,
|
||||
): Promise<void> {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities.startCommunityAuthentication`)
|
||||
): Promise<StartCommunityAuthenticationResult> {
|
||||
const methodLogger = createLogger('startCommunityAuthentication')
|
||||
const handshakeID = randombytes_random().toString()
|
||||
const fedComBPublicKey = new Ed25519PublicKey(fedComB.publicKey)
|
||||
methodLogger.addContext('handshakeID', handshakeID)
|
||||
methodLogger.debug(`startCommunityAuthentication()...`, {
|
||||
fedComB: new FederatedCommunityLoggingView(fedComB),
|
||||
})
|
||||
const homeComA = await getHomeCommunity()
|
||||
methodLogger.debug('homeComA', new CommunityLoggingView(homeComA!))
|
||||
const homeFedComA = await DbFederatedCommunity.findOneByOrFail({
|
||||
foreign: false,
|
||||
apiVersion: CONFIG_CORE.FEDERATION_BACKEND_SEND_ON_API,
|
||||
})
|
||||
methodLogger.debug('homeFedComA', new FederatedCommunityLoggingView(homeFedComA))
|
||||
const comB = await DbCommunity.findOneByOrFail({ publicKey: fedComB.publicKey })
|
||||
methodLogger.debug('started with comB:', new CommunityLoggingView(comB))
|
||||
methodLogger.debug(`start with public key ${fedComBPublicKey.asHex()}`)
|
||||
const homeComA = await getHomeCommunityWithFederatedCommunityOrFail(fedComB.apiVersion)
|
||||
// methodLogger.debug('homeComA', new CommunityLoggingView(homeComA))
|
||||
const homeFedComA = getFederatedCommunityWithApiOrFail(homeComA, fedComB.apiVersion)
|
||||
|
||||
const comB = await getCommunityByPublicKeyOrFail(fedComBPublicKey)
|
||||
// methodLogger.debug('started with comB:', new CommunityLoggingView(comB))
|
||||
// check if communityUuid is not a valid v4Uuid
|
||||
try {
|
||||
if (
|
||||
comB &&
|
||||
((comB.communityUuid === null && comB.authenticatedAt === null) ||
|
||||
(comB.communityUuid !== null &&
|
||||
(!validateUUID(comB.communityUuid) ||
|
||||
versionUUID(comB.communityUuid!) !== 4)))
|
||||
) {
|
||||
methodLogger.debug('comB.uuid is null or is a not valid v4Uuid...', comB.communityUuid || 'null', comB.authenticatedAt || 'null')
|
||||
const client = AuthenticationClientFactory.getInstance(fedComB)
|
||||
|
||||
if (client instanceof V1_0_AuthenticationClient) {
|
||||
if (!comB.publicJwtKey) {
|
||||
throw new Error('Public JWT key still not exist for comB ' + comB.name)
|
||||
}
|
||||
//create JWT with url in payload encrypted by foreignCom.publicJwtKey and signed with homeCom.privateJwtKey
|
||||
const payload = new OpenConnectionJwtPayloadType(handshakeID,
|
||||
ensureUrlEndsWithSlash(homeFedComA.endPoint).concat(homeFedComA.apiVersion),
|
||||
)
|
||||
methodLogger.debug('payload', payload)
|
||||
const jws = await encryptAndSign(payload, homeComA!.privateJwtKey!, comB.publicJwtKey!)
|
||||
methodLogger.debug('jws', jws)
|
||||
// prepare the args for the client invocation
|
||||
const args = new EncryptedTransferArgs()
|
||||
args.publicKey = homeComA!.publicKey.toString('hex')
|
||||
args.jwt = jws
|
||||
args.handshakeID = handshakeID
|
||||
methodLogger.debug('before client.openConnection() args:', args)
|
||||
const result = await client.openConnection(args)
|
||||
if (result) {
|
||||
methodLogger.debug(`successful initiated at community:`, fedComB.endPoint)
|
||||
} else {
|
||||
methodLogger.error(`can't initiate at community:`, fedComB.endPoint)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
methodLogger.debug(`comB.communityUuid is already a valid v4Uuid ${ comB.communityUuid || 'null' } and was authenticated at ${ comB.authenticatedAt || 'null'}`)
|
||||
}
|
||||
} catch (err) {
|
||||
methodLogger.error(`Error:`, err)
|
||||
|
||||
// communityAuthenticatedSchema.safeParse return true
|
||||
// - if communityUuid is a valid v4Uuid and
|
||||
// - if authenticatedAt is a valid date
|
||||
if (communityAuthenticatedSchema.safeParse(comB).success) {
|
||||
methodLogger.debug(`comB.communityUuid is already a valid v4Uuid ${ comB.communityUuid || 'null' } and was authenticated at ${ comB.authenticatedAt || 'null'}`)
|
||||
return StartCommunityAuthenticationResult.ALREADY_AUTHENTICATED
|
||||
}
|
||||
methodLogger.removeContext('handshakeID')
|
||||
/*methodLogger.debug('comB.uuid is null or is a not valid v4Uuid...',
|
||||
comB.communityUuid || 'null', comB.authenticatedAt || 'null'
|
||||
)*/
|
||||
|
||||
// check if a authentication is already in progress
|
||||
const existingState = await findPendingCommunityHandshake(fedComBPublicKey, fedComB.apiVersion)
|
||||
if (existingState) {
|
||||
const stateLogic = new CommunityHandshakeStateLogic(existingState)
|
||||
// retry on timeout or failure
|
||||
if (!(await stateLogic.isTimeoutUpdate())) {
|
||||
// authentication with community and api version is still in progress and it is not timeout yet
|
||||
methodLogger.debug('existingState, so we exit here', new CommunityHandshakeStateLoggingView(existingState))
|
||||
return StartCommunityAuthenticationResult.ALREADY_IN_PROGRESS
|
||||
}
|
||||
}
|
||||
|
||||
const client = AuthenticationClientFactory.getInstance(fedComB)
|
||||
|
||||
if (client instanceof V1_0_AuthenticationClient) {
|
||||
if (!comB.publicJwtKey) {
|
||||
throw new Error(`Public JWT key still not exist for comB ${comB.name}`)
|
||||
}
|
||||
const state = new DbCommunityHandshakeState()
|
||||
state.publicKey = fedComBPublicKey.asBuffer()
|
||||
state.apiVersion = fedComB.apiVersion
|
||||
state.status = CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION
|
||||
state.handshakeId = parseInt(handshakeID)
|
||||
await state.save()
|
||||
methodLogger.debug('[START_COMMUNITY_AUTHENTICATION] community handshake state created')
|
||||
|
||||
//create JWT with url in payload encrypted by foreignCom.publicJwtKey and signed with homeCom.privateJwtKey
|
||||
const payload = new OpenConnectionJwtPayloadType(handshakeID,
|
||||
ensureUrlEndsWithSlash(homeFedComA.endPoint).concat(homeFedComA.apiVersion),
|
||||
)
|
||||
// methodLogger.debug('payload', payload)
|
||||
const jws = await encryptAndSign(payload, homeComA!.privateJwtKey!, comB.publicJwtKey!)
|
||||
// methodLogger.debug('jws', jws)
|
||||
// prepare the args for the client invocation
|
||||
const args = new EncryptedTransferArgs()
|
||||
const homeComAPublicKey = new Ed25519PublicKey(homeComA!.publicKey)
|
||||
args.publicKey = homeComAPublicKey.asHex()
|
||||
args.jwt = jws
|
||||
args.handshakeID = handshakeID
|
||||
// methodLogger.debug('before client.openConnection() args:', args)
|
||||
const result = await client.openConnection(args)
|
||||
if (result) {
|
||||
methodLogger.debug(`successful initiated at community:`, fedComB.endPoint)
|
||||
} else {
|
||||
const errorMsg = `can't initiate at community: ${fedComB.endPoint}`
|
||||
methodLogger.error(errorMsg)
|
||||
state.status = CommunityHandshakeStateType.FAILED
|
||||
state.lastError = errorMsg
|
||||
}
|
||||
await state.save()
|
||||
}
|
||||
return StartCommunityAuthenticationResult.SUCCESSFULLY_STARTED
|
||||
}
|
||||
|
||||
@ -103,7 +103,7 @@ describe('validate Communities', () => {
|
||||
return {
|
||||
data: {
|
||||
getPublicKey: {
|
||||
publicKey: 'somePubKey',
|
||||
publicKey: '2222222222222222222222222222222222222222222222222222222222222222',
|
||||
},
|
||||
},
|
||||
} as Response<unknown>
|
||||
@ -170,8 +170,8 @@ describe('validate Communities', () => {
|
||||
it('logs not matching publicKeys', () => {
|
||||
expect(logger.debug).toBeCalledWith(
|
||||
'received not matching publicKey:',
|
||||
'somePubKey',
|
||||
expect.stringMatching('11111111111111111111111111111111'),
|
||||
'2222222222222222222222222222222222222222222222222222222222222222',
|
||||
expect.stringMatching('1111111111111111111111111111111100000000000000000000000000000000'),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import {
|
||||
Community as DbCommunity,
|
||||
FederatedCommunity as DbFederatedCommunity,
|
||||
FederatedCommunityLoggingView,
|
||||
getHomeCommunity,
|
||||
} from 'database'
|
||||
import { IsNull } from 'typeorm'
|
||||
@ -11,7 +10,7 @@ import { FederationClient as V1_0_FederationClient } from '@/federation/client/1
|
||||
import { PublicCommunityInfo } from '@/federation/client/1_0/model/PublicCommunityInfo'
|
||||
import { FederationClientFactory } from '@/federation/client/FederationClientFactory'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { createKeyPair } from 'shared'
|
||||
import { createKeyPair, Ed25519PublicKey } from 'shared'
|
||||
import { getLogger } from 'log4js'
|
||||
import { startCommunityAuthentication } from './authenticateCommunities'
|
||||
import { PublicCommunityInfoLoggingView } from './client/1_0/logging/PublicCommunityInfoLogging.view'
|
||||
@ -45,27 +44,31 @@ export async function validateCommunities(): Promise<void> {
|
||||
|
||||
logger.debug(`found ${dbFederatedCommunities.length} dbCommunities`)
|
||||
for (const dbFedComB of dbFederatedCommunities) {
|
||||
logger.debug('dbFedComB', new FederatedCommunityLoggingView(dbFedComB))
|
||||
logger.debug(`verify federation community: ${dbFedComB.endPoint}${dbFedComB.apiVersion}`)
|
||||
const apiValueStrings: string[] = Object.values(ApiVersionType)
|
||||
logger.debug(`suppported ApiVersions=`, apiValueStrings)
|
||||
if (!apiValueStrings.includes(dbFedComB.apiVersion)) {
|
||||
logger.debug('dbFedComB with unsupported apiVersion', dbFedComB.endPoint, dbFedComB.apiVersion)
|
||||
logger.debug(`supported ApiVersions=`, apiValueStrings)
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const client = FederationClientFactory.getInstance(dbFedComB)
|
||||
|
||||
if (client instanceof V1_0_FederationClient) {
|
||||
const pubKey = await client.getPublicKey()
|
||||
if (pubKey && pubKey === dbFedComB.publicKey.toString('hex')) {
|
||||
// throw if key isn't valid hex with length 64
|
||||
const clientPublicKey = new Ed25519PublicKey(await client.getPublicKey())
|
||||
// throw if key isn't valid hex with length 64
|
||||
const fedComBPublicKey = new Ed25519PublicKey(dbFedComB.publicKey)
|
||||
if (clientPublicKey.isSame(fedComBPublicKey)) {
|
||||
await DbFederatedCommunity.update({ id: dbFedComB.id }, { verifiedAt: new Date() })
|
||||
logger.debug(`verified dbFedComB with:`, dbFedComB.endPoint)
|
||||
// logger.debug(`verified dbFedComB with:`, dbFedComB.endPoint)
|
||||
const pubComInfo = await client.getPublicCommunityInfo()
|
||||
if (pubComInfo) {
|
||||
await writeForeignCommunity(dbFedComB, pubComInfo)
|
||||
logger.debug(`wrote response of getPublicCommunityInfo in dbFedComB ${dbFedComB.endPoint}`)
|
||||
try {
|
||||
await startCommunityAuthentication(dbFedComB)
|
||||
const result = await startCommunityAuthentication(dbFedComB)
|
||||
logger.info(`${dbFedComB.endPoint}${dbFedComB.apiVersion} verified, authentication state: ${result}`)
|
||||
} catch (err) {
|
||||
logger.warn(`Warning: Authentication of community ${dbFedComB.endPoint} still ongoing:`, err)
|
||||
}
|
||||
@ -73,7 +76,7 @@ export async function validateCommunities(): Promise<void> {
|
||||
logger.debug('missing result of getPublicCommunityInfo')
|
||||
}
|
||||
} else {
|
||||
logger.debug('received not matching publicKey:', pubKey, dbFedComB.publicKey.toString('hex'))
|
||||
logger.debug('received not matching publicKey:', clientPublicKey.asHex(), fedComBPublicKey.asHex())
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@ -48,6 +48,9 @@ beforeAll(async () => {
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
await cleanDB()
|
||||
// reset id auto increment
|
||||
await DbCommunity.clear()
|
||||
await DbFederatedCommunity.clear()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
2
bunfig.toml
Normal file
2
bunfig.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[install]
|
||||
linker = "hoisted"
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "config-schema",
|
||||
"version": "2.6.1",
|
||||
"version": "2.7.0",
|
||||
"description": "Gradido Config for validate config",
|
||||
"main": "./build/index.js",
|
||||
"types": "./src/index.ts",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "core",
|
||||
"version": "2.6.1",
|
||||
"version": "2.7.0",
|
||||
"description": "Gradido Core Code, High-Level Shared Code, with dependencies on other modules",
|
||||
"main": "./build/index.js",
|
||||
"types": "./src/index.ts",
|
||||
@ -28,6 +28,7 @@
|
||||
"database": "*",
|
||||
"esbuild": "^0.25.2",
|
||||
"i18n": "^0.15.1",
|
||||
"joi": "^17.13.3",
|
||||
"jose": "^4.14.4",
|
||||
"log4js": "^6.9.1",
|
||||
"shared": "*",
|
||||
@ -42,6 +43,7 @@
|
||||
"@types/sodium-native": "^2.3.5",
|
||||
"config-schema": "*",
|
||||
"decimal.js-light": "^2.5.1",
|
||||
"dotenv": "^10.0.0",
|
||||
"graphql-request": "5.0.0",
|
||||
"jest": "27.2.4",
|
||||
"type-graphql": "^1.1.1",
|
||||
|
||||
@ -1,42 +1,41 @@
|
||||
import { EncryptedTransferArgs } from '../model/EncryptedTransferArgs'
|
||||
import { JwtPayloadType } from 'shared'
|
||||
import { Ed25519PublicKey, JwtPayloadType } from 'shared'
|
||||
import { Community as DbCommunity } from 'database'
|
||||
import { getLogger } from 'log4js'
|
||||
import { CommunityLoggingView, getHomeCommunity } from 'database'
|
||||
import { verifyAndDecrypt } from 'shared'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.logic.interpretEncryptedTransferArgs`)
|
||||
const createLogger = (functionName: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.logic.interpretEncryptedTransferArgs.${functionName}`)
|
||||
|
||||
export const interpretEncryptedTransferArgs = async (args: EncryptedTransferArgs): Promise<JwtPayloadType | null> => {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.logic.interpretEncryptedTransferArgs-method`)
|
||||
const methodLogger = createLogger('interpretEncryptedTransferArgs')
|
||||
methodLogger.addContext('handshakeID', args.handshakeID)
|
||||
methodLogger.debug('interpretEncryptedTransferArgs()... args:', args)
|
||||
const argsPublicKey = new Ed25519PublicKey(args.publicKey)
|
||||
// first find with args.publicKey the community 'requestingCom', which starts the request
|
||||
const requestingCom = await DbCommunity.findOneBy({ publicKey: Buffer.from(args.publicKey, 'hex') })
|
||||
// TODO: maybe use community from caller instead of loading it separately
|
||||
const requestingCom = await DbCommunity.findOneBy({ publicKey: argsPublicKey.asBuffer() })
|
||||
if (!requestingCom) {
|
||||
const errmsg = `unknown requesting community with publicKey ${Buffer.from(args.publicKey, 'hex')}`
|
||||
const errmsg = `unknown requesting community with publicKey ${argsPublicKey.asHex()}`
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
}
|
||||
if (!requestingCom.publicJwtKey) {
|
||||
const errmsg = `missing publicJwtKey of requesting community with publicKey ${Buffer.from(args.publicKey, 'hex')}`
|
||||
const errmsg = `missing publicJwtKey of requesting community with publicKey ${argsPublicKey.asHex()}`
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
}
|
||||
methodLogger.debug(`found requestingCom:`, new CommunityLoggingView(requestingCom))
|
||||
// verify the signing of args.jwt with homeCom.privateJwtKey and decrypt args.jwt with requestingCom.publicJwtKey
|
||||
// TODO: maybe use community from caller instead of loading it separately
|
||||
const homeCom = await getHomeCommunity()
|
||||
const jwtPayload = await verifyAndDecrypt(args.handshakeID, args.jwt, homeCom!.privateJwtKey!, requestingCom.publicJwtKey) as JwtPayloadType
|
||||
if (!jwtPayload) {
|
||||
const errmsg = `invalid payload of community with publicKey ${Buffer.from(args.publicKey, 'hex')}`
|
||||
const errmsg = `invalid payload of community with publicKey ${argsPublicKey.asHex()}`
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
}
|
||||
methodLogger.debug('jwtPayload', jwtPayload)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return jwtPayload
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import { Decimal } from 'decimal.js-light'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const'
|
||||
import { PendingTransactionState } from 'shared'
|
||||
// import { LogError } from '@/server/LogError'
|
||||
import { calculateSenderBalance } from 'core'
|
||||
import { calculateSenderBalance } from '../../util/calculateSenderBalance'
|
||||
import { TRANSACTIONS_LOCK, getLastTransaction } from 'database'
|
||||
import { getLogger } from 'log4js'
|
||||
|
||||
|
||||
@ -22,4 +22,5 @@ export * from './util/calculateSenderBalance'
|
||||
export * from './util/utilities'
|
||||
export * from './validation/user'
|
||||
export * from './config/index'
|
||||
export * from './logic'
|
||||
|
||||
|
||||
33
core/src/logic/CommunityHandshakeState.logic.ts
Normal file
33
core/src/logic/CommunityHandshakeState.logic.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { CommunityHandshakeState, CommunityHandshakeStateType } from 'database'
|
||||
import { FEDERATION_AUTHENTICATION_TIMEOUT_MS } from 'shared'
|
||||
|
||||
export class CommunityHandshakeStateLogic {
|
||||
public constructor(private self: CommunityHandshakeState) {}
|
||||
|
||||
/**
|
||||
* Check for expired state and if not, check timeout and update (write into db) to expired state
|
||||
* @returns true if the community handshake state is expired
|
||||
*/
|
||||
public async isTimeoutUpdate(): Promise<boolean> {
|
||||
const timeout = this.isTimeout()
|
||||
if (timeout && this.self.status !== CommunityHandshakeStateType.EXPIRED) {
|
||||
this.self.status = CommunityHandshakeStateType.EXPIRED
|
||||
await this.self.save()
|
||||
}
|
||||
return timeout
|
||||
}
|
||||
|
||||
public isTimeout(): boolean {
|
||||
if (this.self.status === CommunityHandshakeStateType.EXPIRED) {
|
||||
return true
|
||||
}
|
||||
if ((Date.now() - this.self.updatedAt.getTime()) > FEDERATION_AUTHENTICATION_TIMEOUT_MS) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public isFailed(): boolean {
|
||||
return this.self.status === CommunityHandshakeStateType.FAILED
|
||||
}
|
||||
}
|
||||
22
core/src/logic/CommunityHandshakeStateLogic.test.ts
Normal file
22
core/src/logic/CommunityHandshakeStateLogic.test.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { CommunityHandshakeState } from 'database'
|
||||
import { CommunityHandshakeStateLogic } from './CommunityHandshakeState.logic'
|
||||
import { CommunityHandshakeStateType } from 'database'
|
||||
import { FEDERATION_AUTHENTICATION_TIMEOUT_MS } from 'shared'
|
||||
|
||||
describe('CommunityHandshakeStateLogic', () => {
|
||||
it('isTimeout', () => {
|
||||
const state = new CommunityHandshakeState()
|
||||
state.updatedAt = new Date(Date.now() - FEDERATION_AUTHENTICATION_TIMEOUT_MS * 2)
|
||||
state.status = CommunityHandshakeStateType.START_AUTHENTICATION
|
||||
const logic = new CommunityHandshakeStateLogic(state)
|
||||
expect(logic.isTimeout()).toEqual(true)
|
||||
})
|
||||
|
||||
it('isTimeout return false', () => {
|
||||
const state = new CommunityHandshakeState()
|
||||
state.updatedAt = new Date(Date.now())
|
||||
state.status = CommunityHandshakeStateType.START_AUTHENTICATION
|
||||
const logic = new CommunityHandshakeStateLogic(state)
|
||||
expect(logic.isTimeout()).toEqual(false)
|
||||
})
|
||||
})
|
||||
12
core/src/logic/community.logic.ts
Normal file
12
core/src/logic/community.logic.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from 'database'
|
||||
|
||||
export function getFederatedCommunityWithApiOrFail(
|
||||
community: DbCommunity,
|
||||
apiVersion: string
|
||||
): DbFederatedCommunity {
|
||||
const fedCom = community.federatedCommunities?.find((fedCom) => fedCom.apiVersion === apiVersion)
|
||||
if (!fedCom) {
|
||||
throw new Error(`Missing federated community with api version ${apiVersion}`)
|
||||
}
|
||||
return fedCom
|
||||
}
|
||||
2
core/src/logic/index.ts
Normal file
2
core/src/logic/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './CommunityHandshakeState.logic'
|
||||
export * from './community.logic'
|
||||
@ -36,6 +36,11 @@ export const delay = promisify(setTimeout)
|
||||
export const ensureUrlEndsWithSlash = (url: string): string => {
|
||||
return url.endsWith('/') ? url : url.concat('/')
|
||||
}
|
||||
export function splitUrlInEndPointAndApiVersion(url: string): { endPoint: string, apiVersion: string } {
|
||||
const endPoint = url.slice(0, url.lastIndexOf('/') + 1)
|
||||
const apiVersion = url.slice(url.lastIndexOf('/') + 1, url.length)
|
||||
return { endPoint, apiVersion }
|
||||
}
|
||||
/**
|
||||
* Calculates the date representing the first day of the month, a specified number of months prior to a given date.
|
||||
*
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`
|
||||
CREATE TABLE community_handshake_states (
|
||||
id int unsigned NOT NULL AUTO_INCREMENT,
|
||||
handshake_id int unsigned NOT NULL,
|
||||
one_time_code int unsigned NULL DEFAULT NULL,
|
||||
public_key binary(32) NOT NULL,
|
||||
api_version varchar(255) NOT NULL,
|
||||
status varchar(255) NOT NULL DEFAULT 'OPEN_CONNECTION',
|
||||
last_error text,
|
||||
created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_public_key (public_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`)
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`DROP TABLE community_handshake_states;`)
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "database",
|
||||
"version": "2.6.1",
|
||||
"version": "2.7.0",
|
||||
"description": "Gradido Database Tool to execute database migrations",
|
||||
"main": "./build/index.js",
|
||||
"types": "./src/index.ts",
|
||||
@ -40,6 +40,7 @@
|
||||
"@types/faker": "^5.5.9",
|
||||
"@types/geojson": "^7946.0.13",
|
||||
"@types/jest": "27.0.2",
|
||||
"@types/mysql": "^2.15.27",
|
||||
"@types/node": "^18.7.14",
|
||||
"await-semaphore": "^0.1.3",
|
||||
"crypto-random-bigint": "^2.1.1",
|
||||
@ -57,6 +58,7 @@
|
||||
"esbuild": "^0.25.2",
|
||||
"geojson": "^0.5.0",
|
||||
"log4js": "^6.9.1",
|
||||
"mysql": "^2.18.1",
|
||||
"mysql2": "^2.3.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"shared": "*",
|
||||
|
||||
37
database/src/entity/CommunityHandshakeState.ts
Normal file
37
database/src/entity/CommunityHandshakeState.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { CommunityHandshakeStateType } from '../enum'
|
||||
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
|
||||
|
||||
@Entity('community_handshake_states')
|
||||
export class CommunityHandshakeState extends BaseEntity {
|
||||
@PrimaryGeneratedColumn({ unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'handshake_id', type: 'int', unsigned: true })
|
||||
handshakeId: number
|
||||
|
||||
@Column({ name: 'one_time_code', type: 'int', unsigned: true, default: null, nullable: true })
|
||||
oneTimeCode?: number
|
||||
|
||||
@Column({ name: 'public_key', type: 'binary', length: 32 })
|
||||
publicKey: Buffer
|
||||
|
||||
@Column({ name: 'api_version', type: 'varchar', length: 255 })
|
||||
apiVersion: string
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
default: CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION,
|
||||
nullable: false,
|
||||
})
|
||||
status: CommunityHandshakeStateType
|
||||
|
||||
@Column({ name: 'last_error', type: 'text', nullable: true })
|
||||
lastError?: string
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'datetime', precision: 3 })
|
||||
createdAt: Date
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'datetime', precision: 3 })
|
||||
updatedAt: Date
|
||||
}
|
||||
@ -7,6 +7,7 @@ import { Event } from './Event'
|
||||
import { FederatedCommunity } from './FederatedCommunity'
|
||||
import { LoginElopageBuys } from './LoginElopageBuys'
|
||||
import { Migration } from './Migration'
|
||||
import { CommunityHandshakeState } from './CommunityHandshakeState'
|
||||
import { OpenaiThreads } from './OpenaiThreads'
|
||||
import { PendingTransaction } from './PendingTransaction'
|
||||
import { ProjectBranding } from './ProjectBranding'
|
||||
@ -18,6 +19,7 @@ import { UserRole } from './UserRole'
|
||||
|
||||
export {
|
||||
Community,
|
||||
CommunityHandshakeState,
|
||||
Contribution,
|
||||
ContributionLink,
|
||||
ContributionMessage,
|
||||
@ -25,7 +27,7 @@ export {
|
||||
Event,
|
||||
FederatedCommunity,
|
||||
LoginElopageBuys,
|
||||
Migration,
|
||||
Migration,
|
||||
ProjectBranding,
|
||||
OpenaiThreads,
|
||||
PendingTransaction,
|
||||
@ -38,6 +40,7 @@ export {
|
||||
|
||||
export const entities = [
|
||||
Community,
|
||||
CommunityHandshakeState,
|
||||
Contribution,
|
||||
ContributionLink,
|
||||
ContributionMessage,
|
||||
|
||||
9
database/src/enum/CommunityHandshakeStateType.ts
Normal file
9
database/src/enum/CommunityHandshakeStateType.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export enum CommunityHandshakeStateType {
|
||||
START_COMMUNITY_AUTHENTICATION = 'START_COMMUNITY_AUTHENTICATION',
|
||||
START_OPEN_CONNECTION_CALLBACK = 'START_OPEN_CONNECTION_CALLBACK',
|
||||
START_AUTHENTICATION = 'START_AUTHENTICATION',
|
||||
|
||||
SUCCESS = 'SUCCESS',
|
||||
FAILED = 'FAILED',
|
||||
EXPIRED = 'EXPIRED'
|
||||
}
|
||||
1
database/src/enum/index.ts
Normal file
1
database/src/enum/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './CommunityHandshakeStateType'
|
||||
@ -1,64 +1,9 @@
|
||||
import { latestDbVersion } from './detectLastDBVersion'
|
||||
import { Community } from './entity/Community'
|
||||
import { Contribution } from './entity/Contribution'
|
||||
import { ContributionLink } from './entity/ContributionLink'
|
||||
import { ContributionMessage } from './entity/ContributionMessage'
|
||||
import { DltTransaction } from './entity/DltTransaction'
|
||||
import { Event } from './entity/Event'
|
||||
import { FederatedCommunity } from './entity/FederatedCommunity'
|
||||
import { LoginElopageBuys } from './entity/LoginElopageBuys'
|
||||
import { Migration } from './entity/Migration'
|
||||
import { OpenaiThreads } from './entity/OpenaiThreads'
|
||||
import { PendingTransaction } from './entity/PendingTransaction'
|
||||
import { ProjectBranding } from './entity/ProjectBranding'
|
||||
import { Transaction } from './entity/Transaction'
|
||||
import { TransactionLink } from './entity/TransactionLink'
|
||||
import { User } from './entity/User'
|
||||
import { UserContact } from './entity/UserContact'
|
||||
import { UserRole } from './entity/UserRole'
|
||||
|
||||
export {
|
||||
Community,
|
||||
Contribution,
|
||||
ContributionLink,
|
||||
ContributionMessage,
|
||||
DltTransaction,
|
||||
Event,
|
||||
FederatedCommunity,
|
||||
LoginElopageBuys,
|
||||
Migration,
|
||||
ProjectBranding,
|
||||
OpenaiThreads,
|
||||
PendingTransaction,
|
||||
Transaction,
|
||||
TransactionLink,
|
||||
User,
|
||||
UserContact,
|
||||
UserRole,
|
||||
}
|
||||
|
||||
export const entities = [
|
||||
Community,
|
||||
Contribution,
|
||||
ContributionLink,
|
||||
ContributionMessage,
|
||||
DltTransaction,
|
||||
Event,
|
||||
FederatedCommunity,
|
||||
LoginElopageBuys,
|
||||
Migration,
|
||||
ProjectBranding,
|
||||
OpenaiThreads,
|
||||
PendingTransaction,
|
||||
Transaction,
|
||||
TransactionLink,
|
||||
User,
|
||||
UserContact,
|
||||
UserRole,
|
||||
]
|
||||
|
||||
export { latestDbVersion }
|
||||
export { AppDatabase } from './AppDatabase'
|
||||
|
||||
export * from './entity'
|
||||
export * from './logging'
|
||||
export * from './queries'
|
||||
export * from './util'
|
||||
export * from './util'
|
||||
export * from './enum'
|
||||
export { AppDatabase } from './AppDatabase'
|
||||
|
||||
21
database/src/logging/CommunityHandshakeStateLogging.view.ts
Normal file
21
database/src/logging/CommunityHandshakeStateLogging.view.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { CommunityHandshakeState } from '..'
|
||||
import { AbstractLoggingView } from './AbstractLogging.view'
|
||||
|
||||
export class CommunityHandshakeStateLoggingView extends AbstractLoggingView {
|
||||
public constructor(private self: CommunityHandshakeState) {
|
||||
super()
|
||||
}
|
||||
|
||||
public toJSON(): any {
|
||||
return {
|
||||
id: this.self.id,
|
||||
handshakeId: this.self.handshakeId,
|
||||
oneTimeCode: this.self.oneTimeCode,
|
||||
publicKey: this.self.publicKey.toString(this.bufferStringFormat),
|
||||
status: this.self.status,
|
||||
lastError: this.self.lastError,
|
||||
createdAt: this.dateToString(this.self.createdAt),
|
||||
updatedAt: this.dateToString(this.self.updatedAt),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { Community } from '../entity'
|
||||
|
||||
import { FederatedCommunityLoggingView } from './FederatedCommunityLogging.view'
|
||||
import { AbstractLoggingView } from './AbstractLogging.view'
|
||||
|
||||
export class CommunityLoggingView extends AbstractLoggingView {
|
||||
@ -21,6 +21,9 @@ export class CommunityLoggingView extends AbstractLoggingView {
|
||||
creationDate: this.dateToString(this.self.creationDate),
|
||||
createdAt: this.dateToString(this.self.createdAt),
|
||||
updatedAt: this.dateToString(this.self.updatedAt),
|
||||
federatedCommunities: this.self.federatedCommunities?.map(
|
||||
(federatedCommunity) => new FederatedCommunityLoggingView(federatedCommunity)
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import { TransactionLoggingView } from './TransactionLogging.view'
|
||||
import { UserContactLoggingView } from './UserContactLogging.view'
|
||||
import { UserLoggingView } from './UserLogging.view'
|
||||
import { UserRoleLoggingView } from './UserRoleLogging.view'
|
||||
import { CommunityHandshakeStateLoggingView } from './CommunityHandshakeStateLogging.view'
|
||||
|
||||
export {
|
||||
AbstractLoggingView,
|
||||
@ -24,6 +25,7 @@ export {
|
||||
UserContactLoggingView,
|
||||
UserLoggingView,
|
||||
UserRoleLoggingView,
|
||||
CommunityHandshakeStateLoggingView,
|
||||
}
|
||||
|
||||
export const logger = getLogger(LOG4JS_BASE_CATEGORY_NAME)
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from '..'
|
||||
import { AppDatabase } from '../AppDatabase'
|
||||
import { getCommunityByPublicKeyOrFail, getHomeCommunity, getHomeCommunityWithFederatedCommunityOrFail, getReachableCommunities } from './communities'
|
||||
import { createCommunity, createVerifiedFederatedCommunity } from '../seeds/community'
|
||||
import { getHomeCommunity, getReachableCommunities } from './communities'
|
||||
import { Ed25519PublicKey } from 'shared'
|
||||
|
||||
const db = AppDatabase.getInstance()
|
||||
|
||||
@ -39,7 +40,37 @@ describe('community.queries', () => {
|
||||
expect(community?.privateKey).toStrictEqual(homeCom.privateKey)
|
||||
})
|
||||
})
|
||||
describe('getReachableCommunities', () => {
|
||||
describe('getHomeCommunityWithFederatedCommunityOrFail', () => {
|
||||
it('should return the home community with federated communities', async () => {
|
||||
const homeCom = await createCommunity(false)
|
||||
await createVerifiedFederatedCommunity('1_0', 100, homeCom)
|
||||
const community = await getHomeCommunityWithFederatedCommunityOrFail('1_0')
|
||||
expect(community).toBeDefined()
|
||||
expect(community?.federatedCommunities).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should throw if no home community exists', async () => {
|
||||
expect(() => getHomeCommunityWithFederatedCommunityOrFail('1_0')).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('should throw if no federated community exists', async () => {
|
||||
await createCommunity(false)
|
||||
expect(() => getHomeCommunityWithFederatedCommunityOrFail('1_0')).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('load community by public key returned from getHomeCommunityWithFederatedCommunityOrFail', async () => {
|
||||
const homeCom = await createCommunity(false)
|
||||
await createVerifiedFederatedCommunity('1_0', 100, homeCom)
|
||||
const community = await getHomeCommunityWithFederatedCommunityOrFail('1_0')
|
||||
expect(community).toBeDefined()
|
||||
expect(community?.federatedCommunities).toHaveLength(1)
|
||||
const ed25519PublicKey = new Ed25519PublicKey(community.federatedCommunities![0].publicKey)
|
||||
const communityByPublicKey = await getCommunityByPublicKeyOrFail(ed25519PublicKey)
|
||||
expect(communityByPublicKey).toBeDefined()
|
||||
expect(communityByPublicKey?.communityUuid).toBe(homeCom.communityUuid)
|
||||
})
|
||||
})
|
||||
describe('getReachableCommunities', () => {
|
||||
it('home community counts also to reachable communities', async () => {
|
||||
await createCommunity(false)
|
||||
expect(await getReachableCommunities(1000)).toHaveLength(1)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { urlSchema, uuidv4Schema } from 'shared'
|
||||
import { FindOptionsOrder, FindOptionsWhere, IsNull, MoreThanOrEqual, Not } from 'typeorm'
|
||||
import { Community as DbCommunity } from '../entity'
|
||||
import { Ed25519PublicKey, urlSchema, uuidv4Schema } from 'shared'
|
||||
|
||||
/**
|
||||
* Retrieves the home community, i.e., a community that is not foreign.
|
||||
@ -10,7 +10,14 @@ export async function getHomeCommunity(): Promise<DbCommunity | null> {
|
||||
// TODO: Put in Cache, it is needed nearly always
|
||||
// TODO: return only DbCommunity or throw to reduce unnecessary checks, because there should be always a home community
|
||||
return await DbCommunity.findOne({
|
||||
where: { foreign: false },
|
||||
where: { foreign: false }
|
||||
})
|
||||
}
|
||||
|
||||
export async function getHomeCommunityWithFederatedCommunityOrFail(apiVersion: string): Promise<DbCommunity> {
|
||||
return await DbCommunity.findOneOrFail({
|
||||
where: { foreign: false, federatedCommunities: { apiVersion } },
|
||||
relations: { federatedCommunities: true },
|
||||
})
|
||||
}
|
||||
|
||||
@ -44,7 +51,23 @@ export async function getCommunityWithFederatedCommunityByIdentifier(
|
||||
})
|
||||
}
|
||||
|
||||
// returns all reachable communities
|
||||
export async function getCommunityWithFederatedCommunityWithApiOrFail(
|
||||
publicKey: Ed25519PublicKey,
|
||||
apiVersion: string
|
||||
): Promise<DbCommunity> {
|
||||
return await DbCommunity.findOneOrFail({
|
||||
where: { foreign: true, publicKey: publicKey.asBuffer(), federatedCommunities: { apiVersion } },
|
||||
relations: { federatedCommunities: true },
|
||||
})
|
||||
}
|
||||
|
||||
export async function getCommunityByPublicKeyOrFail(publicKey: Ed25519PublicKey): Promise<DbCommunity> {
|
||||
return await DbCommunity.findOneOrFail({
|
||||
where: { publicKey: publicKey.asBuffer() },
|
||||
})
|
||||
}
|
||||
|
||||
// returns all reachable communities
|
||||
// home community and all federated communities which have been verified within the last authenticationTimeoutMs
|
||||
export async function getReachableCommunities(
|
||||
authenticationTimeoutMs: number,
|
||||
@ -63,3 +86,12 @@ export async function getReachableCommunities(
|
||||
order,
|
||||
})
|
||||
}
|
||||
|
||||
export async function getNotReachableCommunities(
|
||||
order?: FindOptionsOrder<DbCommunity>
|
||||
): Promise<DbCommunity[]> {
|
||||
return await DbCommunity.find({
|
||||
where: { authenticatedAt: IsNull(), foreign: true },
|
||||
order,
|
||||
})
|
||||
}
|
||||
|
||||
71
database/src/queries/communityHandshakes.test.ts
Normal file
71
database/src/queries/communityHandshakes.test.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { AppDatabase } from '../AppDatabase'
|
||||
import {
|
||||
CommunityHandshakeState as DbCommunityHandshakeState,
|
||||
Community as DbCommunity,
|
||||
FederatedCommunity as DbFederatedCommunity,
|
||||
findPendingCommunityHandshake,
|
||||
CommunityHandshakeStateType
|
||||
} from '..'
|
||||
import { describe, expect, it, beforeEach, beforeAll, afterAll } from 'vitest'
|
||||
import { createCommunity, createVerifiedFederatedCommunity } from '../seeds/community'
|
||||
import { Ed25519PublicKey } from 'shared'
|
||||
import { randomBytes } from 'node:crypto'
|
||||
|
||||
const db = AppDatabase.getInstance()
|
||||
|
||||
beforeAll(async () => {
|
||||
await db.init()
|
||||
})
|
||||
afterAll(async () => {
|
||||
await db.destroy()
|
||||
})
|
||||
|
||||
async function createCommunityHandshakeState(publicKey: Buffer) {
|
||||
const state = new DbCommunityHandshakeState()
|
||||
state.publicKey = publicKey
|
||||
state.apiVersion = '1_0'
|
||||
state.status = CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION
|
||||
state.handshakeId = 1
|
||||
await state.save()
|
||||
}
|
||||
|
||||
describe('communityHandshakes', () => {
|
||||
// clean db for every test case
|
||||
beforeEach(async () => {
|
||||
await DbCommunity.clear()
|
||||
await DbFederatedCommunity.clear()
|
||||
await DbCommunityHandshakeState.clear()
|
||||
})
|
||||
|
||||
it('should find pending community handshake by public key', async () => {
|
||||
const com1 = await createCommunity(false)
|
||||
await createVerifiedFederatedCommunity('1_0', 100, com1)
|
||||
await createCommunityHandshakeState(com1.publicKey)
|
||||
const communityHandshakeState = await findPendingCommunityHandshake(new Ed25519PublicKey(com1.publicKey), '1_0')
|
||||
expect(communityHandshakeState).toBeDefined()
|
||||
expect(communityHandshakeState).toMatchObject({
|
||||
publicKey: com1.publicKey,
|
||||
apiVersion: '1_0',
|
||||
status: CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION,
|
||||
handshakeId: 1
|
||||
})
|
||||
})
|
||||
|
||||
it('update state', async () => {
|
||||
const publicKey = new Ed25519PublicKey(randomBytes(32))
|
||||
await createCommunityHandshakeState(publicKey.asBuffer())
|
||||
const communityHandshakeState = await findPendingCommunityHandshake(publicKey, '1_0')
|
||||
expect(communityHandshakeState).toBeDefined()
|
||||
communityHandshakeState!.status = CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK
|
||||
await communityHandshakeState!.save()
|
||||
const communityHandshakeState2 = await findPendingCommunityHandshake(publicKey, '1_0')
|
||||
const states = await DbCommunityHandshakeState.find()
|
||||
expect(communityHandshakeState2).toBeDefined()
|
||||
expect(communityHandshakeState2).toMatchObject({
|
||||
publicKey: publicKey.asBuffer(),
|
||||
apiVersion: '1_0',
|
||||
status: CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK,
|
||||
handshakeId: 1
|
||||
})
|
||||
})
|
||||
})
|
||||
35
database/src/queries/communityHandshakes.ts
Normal file
35
database/src/queries/communityHandshakes.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Not, In } from 'typeorm'
|
||||
import { CommunityHandshakeState, CommunityHandshakeStateType} from '..'
|
||||
import { Ed25519PublicKey } from 'shared'
|
||||
|
||||
/**
|
||||
* Find a pending community handshake by public key.
|
||||
* @param publicKey The public key of the community.
|
||||
* @param apiVersion The API version of the community.
|
||||
* @param status The status of the community handshake. Optional, if not set, it will find a pending community handshake.
|
||||
* @returns The CommunityHandshakeState with associated federated community and community.
|
||||
*/
|
||||
export function findPendingCommunityHandshake(
|
||||
publicKey: Ed25519PublicKey, apiVersion: string, status?: CommunityHandshakeStateType
|
||||
): Promise<CommunityHandshakeState | null> {
|
||||
return CommunityHandshakeState.findOne({
|
||||
where: {
|
||||
publicKey: publicKey.asBuffer(),
|
||||
apiVersion,
|
||||
status: status || Not(In([
|
||||
CommunityHandshakeStateType.EXPIRED,
|
||||
CommunityHandshakeStateType.FAILED,
|
||||
CommunityHandshakeStateType.SUCCESS
|
||||
]))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function findPendingCommunityHandshakeOrFailByOneTimeCode(
|
||||
oneTimeCode: number
|
||||
): Promise<CommunityHandshakeState> {
|
||||
return CommunityHandshakeState.findOneOrFail({
|
||||
where: { oneTimeCode },
|
||||
})
|
||||
}
|
||||
|
||||
@ -6,5 +6,6 @@ export * from './pendingTransactions'
|
||||
export * from './transactionLinks'
|
||||
export * from './transactions'
|
||||
export * from './user'
|
||||
export * from './communityHandshakes'
|
||||
|
||||
export const LOG4JS_QUERIES_CATEGORY_NAME = `${LOG4JS_BASE_CATEGORY_NAME}.queries`
|
||||
|
||||
@ -2,41 +2,55 @@ import { randomBytes } from 'node:crypto'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { Community, FederatedCommunity } from '../entity'
|
||||
|
||||
export async function createCommunity(foreign: boolean, save: boolean = true): Promise<Community> {
|
||||
const community = new Community()
|
||||
community.publicKey = randomBytes(32)
|
||||
community.communityUuid = uuidv4()
|
||||
community.name = 'HomeCommunity-name'
|
||||
community.creationDate = new Date()
|
||||
|
||||
if (foreign) {
|
||||
community.foreign = true
|
||||
community.name = 'ForeignCommunity-name'
|
||||
community.description = 'ForeignCommunity-description'
|
||||
community.url = `http://foreign-${Math.random()}/api`
|
||||
community.authenticatedAt = new Date()
|
||||
} else {
|
||||
community.foreign = false
|
||||
// todo: generate valid public/private key pair (ed25519)
|
||||
community.privateKey = randomBytes(64)
|
||||
/**
|
||||
* Creates a community.
|
||||
* @param foreign
|
||||
* @param store if true, write to db, default: true
|
||||
* @returns
|
||||
*/
|
||||
export async function createCommunity(foreign: boolean, store: boolean = true): Promise<Community> {
|
||||
const community = new Community()
|
||||
community.publicKey = randomBytes(32)
|
||||
community.communityUuid = uuidv4()
|
||||
community.name = 'HomeCommunity-name'
|
||||
community.description = 'HomeCommunity-description'
|
||||
community.url = 'http://localhost/api'
|
||||
}
|
||||
return save ? await community.save() : community
|
||||
community.creationDate = new Date()
|
||||
|
||||
if(foreign) {
|
||||
community.foreign = true
|
||||
community.name = 'ForeignCommunity-name'
|
||||
community.description = 'ForeignCommunity-description'
|
||||
community.url = `http://foreign-${Math.random()}/api`
|
||||
community.authenticatedAt = new Date()
|
||||
} else {
|
||||
community.foreign = false
|
||||
// todo: generate valid public/private key pair (ed25519)
|
||||
community.privateKey = randomBytes(64)
|
||||
community.name = 'HomeCommunity-name'
|
||||
community.description = 'HomeCommunity-description'
|
||||
community.url = 'http://localhost/api'
|
||||
}
|
||||
return store ? await community.save() : community
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a verified federated community.
|
||||
* @param apiVersion
|
||||
* @param verifiedBeforeMs time in ms before the current time
|
||||
* @param community
|
||||
* @param store if true, write to db, default: true
|
||||
* @returns
|
||||
*/
|
||||
export async function createVerifiedFederatedCommunity(
|
||||
apiVersion: string,
|
||||
verifiedBeforeMs: number,
|
||||
community: Community,
|
||||
save: boolean = true,
|
||||
apiVersion: string,
|
||||
verifiedBeforeMs: number,
|
||||
community: Community,
|
||||
store: boolean = true
|
||||
): Promise<FederatedCommunity> {
|
||||
const federatedCommunity = new FederatedCommunity()
|
||||
federatedCommunity.apiVersion = apiVersion
|
||||
federatedCommunity.endPoint = community.url
|
||||
federatedCommunity.publicKey = community.publicKey
|
||||
federatedCommunity.community = community
|
||||
federatedCommunity.verifiedAt = new Date(Date.now() - verifiedBeforeMs)
|
||||
return save ? await federatedCommunity.save() : federatedCommunity
|
||||
const federatedCommunity = new FederatedCommunity()
|
||||
federatedCommunity.apiVersion = apiVersion
|
||||
federatedCommunity.endPoint = community.url
|
||||
federatedCommunity.publicKey = community.publicKey
|
||||
federatedCommunity.community = community
|
||||
federatedCommunity.verifiedAt = new Date(Date.now() - verifiedBeforeMs)
|
||||
return store ? await federatedCommunity.save() : federatedCommunity
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dht-node",
|
||||
"version": "2.6.1",
|
||||
"version": "2.7.0",
|
||||
"description": "Gradido dht-node module",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/",
|
||||
@ -38,7 +38,7 @@
|
||||
"@types/uuid": "^8.3.4",
|
||||
"config-schema": "*",
|
||||
"database": "*",
|
||||
"dotenv": "10.0.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"esbuild": "^0.25.3",
|
||||
"jest": "27.5.1",
|
||||
"joi": "17.13.3",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "federation",
|
||||
"version": "2.6.1",
|
||||
"version": "2.7.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",
|
||||
@ -31,6 +31,7 @@
|
||||
"@swc/cli": "^0.7.3",
|
||||
"@swc/core": "^1.11.24",
|
||||
"@swc/helpers": "^0.5.17",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "4.17.21",
|
||||
"@types/jest": "27.0.2",
|
||||
"@types/lodash.clonedeep": "^4.5.6",
|
||||
@ -46,7 +47,8 @@
|
||||
"cors": "2.8.5",
|
||||
"database": "*",
|
||||
"decimal.js-light": "^2.5.1",
|
||||
"dotenv": "10.0.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"esbuild": "^0.25.3",
|
||||
"express": "^4.17.21",
|
||||
"express-slow-down": "^2.0.1",
|
||||
"graphql": "15.10.1",
|
||||
@ -61,8 +63,10 @@
|
||||
"nodemon": "^2.0.7",
|
||||
"prettier": "^3.5.3",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"shared": "*",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-jest": "27.0.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.1.1",
|
||||
"type-graphql": "^1.1.1",
|
||||
"typeorm": "^0.3.25",
|
||||
|
||||
@ -1,19 +1,31 @@
|
||||
import { CONFIG } from '@/config'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { EncryptedTransferArgs, interpretEncryptedTransferArgs } from 'core'
|
||||
import { CommunityHandshakeStateLogic, EncryptedTransferArgs, interpretEncryptedTransferArgs, splitUrlInEndPointAndApiVersion } from 'core'
|
||||
import {
|
||||
CommunityLoggingView,
|
||||
Community as DbCommunity,
|
||||
CommunityHandshakeStateLoggingView,
|
||||
CommunityHandshakeState as DbCommunityHandshakeState,
|
||||
CommunityHandshakeStateType,
|
||||
FederatedCommunity as DbFedCommunity,
|
||||
FederatedCommunityLoggingView,
|
||||
getHomeCommunity,
|
||||
findPendingCommunityHandshakeOrFailByOneTimeCode,
|
||||
getCommunityByPublicKeyOrFail,
|
||||
} from 'database'
|
||||
import { getLogger } from 'log4js'
|
||||
import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, OpenConnectionJwtPayloadType, uint32Schema, uuidv4Schema } from 'shared'
|
||||
import {
|
||||
AuthenticationJwtPayloadType,
|
||||
AuthenticationResponseJwtPayloadType,
|
||||
Ed25519PublicKey,
|
||||
encryptAndSign,
|
||||
OpenConnectionCallbackJwtPayloadType,
|
||||
OpenConnectionJwtPayloadType,
|
||||
uint32Schema,
|
||||
uuidv4Schema
|
||||
} from 'shared'
|
||||
import { Arg, Mutation, Resolver } from 'type-graphql'
|
||||
import { startAuthentication, startOpenConnectionCallback } from '../util/authenticateCommunity'
|
||||
|
||||
const createLogger = (method: string ) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.${method}`)
|
||||
// TODO: think about the case, when we have a higher api version, which still use this resolver
|
||||
const apiVersion = '1_0'
|
||||
const createLogger = (method: string ) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.${apiVersion}.resolver.AuthenticationResolver.${method}`)
|
||||
|
||||
@Resolver()
|
||||
export class AuthenticationResolver {
|
||||
@ -24,45 +36,38 @@ export class AuthenticationResolver {
|
||||
): Promise<boolean> {
|
||||
const methodLogger = createLogger('openConnection')
|
||||
methodLogger.addContext('handshakeID', args.handshakeID)
|
||||
methodLogger.debug(`openConnection() via apiVersion=1_0:`, args)
|
||||
const argsPublicKey = new Ed25519PublicKey(args.publicKey)
|
||||
methodLogger.debug(`start via apiVersion=${apiVersion}, public key: ${argsPublicKey.asHex()}`)
|
||||
try {
|
||||
const openConnectionJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionJwtPayloadType
|
||||
methodLogger.debug('openConnectionJwtPayload', openConnectionJwtPayload)
|
||||
methodLogger.debug(`openConnectionJwtPayload url: ${openConnectionJwtPayload.url}`)
|
||||
if (!openConnectionJwtPayload) {
|
||||
const errmsg = `invalid OpenConnection payload of requesting community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return true
|
||||
throw new Error(`invalid OpenConnection payload of requesting community with publicKey ${argsPublicKey.asHex()}`)
|
||||
}
|
||||
if (openConnectionJwtPayload.tokentype !== OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE) {
|
||||
const errmsg = `invalid tokentype of community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return true
|
||||
throw new Error(`invalid tokentype: ${openConnectionJwtPayload.tokentype} of community with publicKey ${argsPublicKey.asHex()}`)
|
||||
}
|
||||
if (!openConnectionJwtPayload.url) {
|
||||
const errmsg = `invalid url of community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return true
|
||||
throw new Error(`invalid url of community with publicKey ${argsPublicKey.asHex()}`)
|
||||
}
|
||||
methodLogger.debug(`vor DbFedCommunity.findOneByOrFail()...`, { publicKey: args.publicKey })
|
||||
const fedComA = await DbFedCommunity.findOneByOrFail({ publicKey: Buffer.from(args.publicKey, 'hex') })
|
||||
methodLogger.debug(`nach DbFedCommunity.findOneByOrFail()...`, fedComA)
|
||||
methodLogger.debug('fedComA', new FederatedCommunityLoggingView(fedComA))
|
||||
// methodLogger.debug(`before DbFedCommunity.findOneByOrFail()...`, { publicKey: argsPublicKey.asHex() })
|
||||
const fedComA = await DbFedCommunity.findOneByOrFail({ publicKey: argsPublicKey.asBuffer() })
|
||||
// methodLogger.debug(`after DbFedCommunity.findOneByOrFail()...`, new FederatedCommunityLoggingView(fedComA))
|
||||
if (!openConnectionJwtPayload.url.startsWith(fedComA.endPoint)) {
|
||||
const errmsg = `invalid url of community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return true
|
||||
throw new Error(`invalid url of community with publicKey ${argsPublicKey.asHex()}`)
|
||||
}
|
||||
if (fedComA.apiVersion !== apiVersion) {
|
||||
throw new Error(`invalid apiVersion: ${fedComA.apiVersion} of community with publicKey ${argsPublicKey.asHex()}`)
|
||||
}
|
||||
|
||||
// no await to respond immediately and invoke callback-request asynchronously
|
||||
void startOpenConnectionCallback(args.handshakeID, args.publicKey, CONFIG.FEDERATION_API)
|
||||
// important: startOpenConnectionCallback must catch all exceptions them self, or server will crash!
|
||||
void startOpenConnectionCallback(args.handshakeID, argsPublicKey, fedComA)
|
||||
methodLogger.debug('openConnection() successfully initiated callback and returns true immediately...')
|
||||
return true
|
||||
} catch (err) {
|
||||
methodLogger.error('invalid jwt token:', err)
|
||||
// no infos to the caller
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -74,37 +79,29 @@ export class AuthenticationResolver {
|
||||
): Promise<boolean> {
|
||||
const methodLogger = createLogger('openConnectionCallback')
|
||||
methodLogger.addContext('handshakeID', args.handshakeID)
|
||||
methodLogger.debug(`openConnectionCallback() via apiVersion=1_0 ...`, args)
|
||||
methodLogger.debug(`start via apiVersion=${apiVersion}, public key: ${args.publicKey}`)
|
||||
try {
|
||||
// decrypt args.url with homeCom.privateJwtKey and verify signing with callbackFedCom.publicKey
|
||||
const openConnectionCallbackJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionCallbackJwtPayloadType
|
||||
if (!openConnectionCallbackJwtPayload) {
|
||||
const errmsg = `invalid OpenConnectionCallback payload of requesting community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return true
|
||||
throw new Error(`invalid OpenConnectionCallback payload of requesting community with publicKey ${args.publicKey}`)
|
||||
}
|
||||
|
||||
const endPoint = openConnectionCallbackJwtPayload.url.slice(0, openConnectionCallbackJwtPayload.url.lastIndexOf('/') + 1)
|
||||
const apiVersion = openConnectionCallbackJwtPayload.url.slice(openConnectionCallbackJwtPayload.url.lastIndexOf('/') + 1, openConnectionCallbackJwtPayload.url.length)
|
||||
methodLogger.debug(`search fedComB per:`, endPoint, apiVersion)
|
||||
const { endPoint, apiVersion } = splitUrlInEndPointAndApiVersion(openConnectionCallbackJwtPayload.url)
|
||||
// methodLogger.debug(`search fedComB per:`, endPoint, apiVersion)
|
||||
const fedComB = await DbFedCommunity.findOneBy({ endPoint, apiVersion })
|
||||
if (!fedComB) {
|
||||
const errmsg = `unknown callback community with url` + openConnectionCallbackJwtPayload.url
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return true
|
||||
throw new Error(`unknown callback community for ${endPoint}${apiVersion}`)
|
||||
}
|
||||
methodLogger.debug(
|
||||
`found fedComB and start authentication:`,
|
||||
new FederatedCommunityLoggingView(fedComB),
|
||||
`found fedComB and start authentication: ${fedComB.endPoint}${fedComB.apiVersion}`,
|
||||
)
|
||||
// no await to respond immediately and invoke authenticate-request asynchronously
|
||||
void startAuthentication(args.handshakeID, openConnectionCallbackJwtPayload.oneTimeCode, fedComB)
|
||||
methodLogger.debug('openConnectionCallback() successfully initiated authentication and returns true immediately...')
|
||||
// methodLogger.debug('openConnectionCallback() successfully initiated authentication and returns true immediately...')
|
||||
return true
|
||||
} catch (err) {
|
||||
methodLogger.error('invalid jwt token:', err)
|
||||
// no infos to the caller
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -116,51 +113,80 @@ export class AuthenticationResolver {
|
||||
): Promise<string | null> {
|
||||
const methodLogger = createLogger('authenticate')
|
||||
methodLogger.addContext('handshakeID', args.handshakeID)
|
||||
methodLogger.debug(`authenticate() via apiVersion=1_0 ...`, args)
|
||||
methodLogger.debug(`start via apiVersion=${apiVersion}, public key: ${args.publicKey}`)
|
||||
let state: DbCommunityHandshakeState | null = null
|
||||
const argsPublicKey = new Ed25519PublicKey(args.publicKey)
|
||||
try {
|
||||
const authArgs = await interpretEncryptedTransferArgs(args) as AuthenticationJwtPayloadType
|
||||
// methodLogger.debug(`interpreted authentication payload...authArgs:`, authArgs)
|
||||
if (!authArgs) {
|
||||
const errmsg = `invalid authentication payload of requesting community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return null
|
||||
methodLogger.debug(`interpretEncryptedTransferArgs was called with`, args)
|
||||
throw new Error(`invalid authentication payload of requesting community with publicKey ${argsPublicKey.asHex()}`)
|
||||
}
|
||||
if (!uint32Schema.safeParse(Number(authArgs.oneTimeCode)).success) {
|
||||
const errmsg = `invalid oneTimeCode: ${authArgs.oneTimeCode} for community with publicKey ${authArgs.publicKey}, expect uint32`
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return null
|
||||
const validOneTimeCode = uint32Schema.safeParse(Number(authArgs.oneTimeCode))
|
||||
if (!validOneTimeCode.success) {
|
||||
throw new Error(
|
||||
`invalid oneTimeCode: ${authArgs.oneTimeCode} for community with publicKey ${argsPublicKey.asHex()}, expect uint32`
|
||||
)
|
||||
}
|
||||
const authCom = await DbCommunity.findOneByOrFail({ communityUuid: authArgs.oneTimeCode })
|
||||
|
||||
state = await findPendingCommunityHandshakeOrFailByOneTimeCode(validOneTimeCode.data)
|
||||
const stateLogic = new CommunityHandshakeStateLogic(state)
|
||||
if (
|
||||
(await stateLogic.isTimeoutUpdate()) ||
|
||||
state.status !== CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK
|
||||
) {
|
||||
throw new Error('No valid pending community handshake found')
|
||||
}
|
||||
state.status = CommunityHandshakeStateType.SUCCESS
|
||||
await state.save()
|
||||
methodLogger.debug('[SUCCESS] community handshake state updated')
|
||||
|
||||
// methodLogger.debug(`search community per oneTimeCode:`, authArgs.oneTimeCode)
|
||||
const authCom = await getCommunityByPublicKeyOrFail(argsPublicKey)
|
||||
if (authCom) {
|
||||
methodLogger.debug('found authCom:', new CommunityLoggingView(authCom))
|
||||
if (authCom.publicKey !== authArgs.publicKey) {
|
||||
const errmsg = `corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${authArgs.publicKey}`
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return null
|
||||
methodLogger.debug(`found authCom ${authCom.name}`)
|
||||
const authComPublicKey = new Ed25519PublicKey(authCom.publicKey)
|
||||
// methodLogger.debug('authCom.publicKey', authComPublicKey.asHex())
|
||||
// methodLogger.debug('args.publicKey', argsPublicKey.asHex())
|
||||
if (!authComPublicKey.isSame(argsPublicKey)) {
|
||||
throw new Error(
|
||||
`corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${argsPublicKey.asHex()}`
|
||||
)
|
||||
}
|
||||
const communityUuid = uuidv4Schema.safeParse(authArgs.uuid)
|
||||
if (!communityUuid.success) {
|
||||
const errmsg = `invalid uuid: ${authArgs.uuid} for community with publicKey ${authArgs.publicKey}`
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return null
|
||||
throw new Error(
|
||||
`invalid uuid: ${authArgs.uuid} for community with publicKey ${authComPublicKey.asHex()}`
|
||||
)
|
||||
}
|
||||
authCom.communityUuid = communityUuid.data
|
||||
authCom.authenticatedAt = new Date()
|
||||
await DbCommunity.save(authCom)
|
||||
methodLogger.debug('store authCom.uuid successfully:', new CommunityLoggingView(authCom))
|
||||
await authCom.save()
|
||||
methodLogger.debug(`update authCom.uuid successfully with ${authCom.communityUuid} at ${authCom.authenticatedAt}`)
|
||||
|
||||
const homeComB = await getHomeCommunity()
|
||||
if (homeComB?.communityUuid) {
|
||||
const responseArgs = new AuthenticationResponseJwtPayloadType(args.handshakeID,homeComB.communityUuid)
|
||||
const responseJwt = await encryptAndSign(responseArgs, homeComB.privateJwtKey!, authCom.publicJwtKey!)
|
||||
return responseJwt
|
||||
}
|
||||
} else {
|
||||
throw new Error(`community with publicKey ${argsPublicKey.asHex()} not found`)
|
||||
}
|
||||
return null
|
||||
} catch (err) {
|
||||
methodLogger.error('invalid jwt token:', err)
|
||||
if (state) {
|
||||
try {
|
||||
state.status = CommunityHandshakeStateType.FAILED
|
||||
state.lastError = String(err)
|
||||
await state.save()
|
||||
} catch (err) {
|
||||
methodLogger.error(`failed to save state`, new CommunityHandshakeStateLoggingView(state), err)
|
||||
}
|
||||
}
|
||||
methodLogger.error(`failed`, err)
|
||||
// no infos to the caller
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,85 +1,118 @@
|
||||
import { EncryptedTransferArgs } from 'core'
|
||||
import { CommunityHandshakeStateLogic, EncryptedTransferArgs, ensureUrlEndsWithSlash } from 'core'
|
||||
import {
|
||||
CommunityLoggingView,
|
||||
CommunityHandshakeStateLoggingView,
|
||||
Community as DbCommunity,
|
||||
FederatedCommunity as DbFedCommunity,
|
||||
FederatedCommunityLoggingView,
|
||||
findPendingCommunityHandshake,
|
||||
getCommunityByPublicKeyOrFail,
|
||||
getHomeCommunity,
|
||||
getHomeCommunityWithFederatedCommunityOrFail,
|
||||
} from 'database'
|
||||
import { getLogger } from 'log4js'
|
||||
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
||||
|
||||
import { AuthenticationClientFactory } from '@/client/AuthenticationClientFactory'
|
||||
import { randombytes_random } from 'sodium-native'
|
||||
|
||||
import { AuthenticationClient as V1_0_AuthenticationClient } from '@/client/1_0/AuthenticationClient'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, uuidv4Schema, verifyAndDecrypt } from 'shared'
|
||||
import {
|
||||
AuthenticationJwtPayloadType,
|
||||
AuthenticationResponseJwtPayloadType,
|
||||
Ed25519PublicKey,
|
||||
encryptAndSign,
|
||||
OpenConnectionCallbackJwtPayloadType,
|
||||
uuidv4Schema,
|
||||
verifyAndDecrypt
|
||||
} from 'shared'
|
||||
import { CommunityHandshakeState as DbCommunityHandshakeState, CommunityHandshakeStateType } from 'database'
|
||||
import { getFederatedCommunityWithApiOrFail } from 'core'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity`)
|
||||
const createLogger = (method: string ) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity.${method}`)
|
||||
|
||||
export async function startOpenConnectionCallback(
|
||||
handshakeID: string,
|
||||
publicKey: string,
|
||||
api: string,
|
||||
publicKey: Ed25519PublicKey,
|
||||
fedComA: DbFedCommunity,
|
||||
): Promise<void> {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity.startOpenConnectionCallback`)
|
||||
const methodLogger = createLogger('startOpenConnectionCallback')
|
||||
methodLogger.addContext('handshakeID', handshakeID)
|
||||
methodLogger.debug(`Authentication: startOpenConnectionCallback() with:`, {
|
||||
publicKey,
|
||||
})
|
||||
methodLogger.debug(`start`)
|
||||
const api = fedComA.apiVersion
|
||||
|
||||
let state: DbCommunityHandshakeState | null = null
|
||||
try {
|
||||
const homeComB = await getHomeCommunity()
|
||||
const homeFedComB = await DbFedCommunity.findOneByOrFail({
|
||||
foreign: false,
|
||||
apiVersion: api,
|
||||
})
|
||||
const comA = await DbCommunity.findOneByOrFail({ publicKey: Buffer.from(publicKey, 'hex') })
|
||||
const fedComA = await DbFedCommunity.findOneByOrFail({
|
||||
foreign: true,
|
||||
apiVersion: api,
|
||||
publicKey: comA.publicKey,
|
||||
})
|
||||
// store oneTimeCode in requestedCom.community_uuid as authenticate-request-identifier
|
||||
// prevent overwriting valid UUID with oneTimeCode, because this request could be initiated at any time from federated community
|
||||
if (uuidv4Schema.safeParse(comA.communityUuid).success) {
|
||||
throw new Error('Community UUID is already a valid UUID')
|
||||
const pendingState = await findPendingCommunityHandshake(publicKey, api, CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK)
|
||||
if (pendingState) {
|
||||
const stateLogic = new CommunityHandshakeStateLogic(pendingState)
|
||||
// retry on timeout or failure
|
||||
if (!(await stateLogic.isTimeoutUpdate())) {
|
||||
// authentication with community and api version is still in progress and it is not timeout yet
|
||||
methodLogger.debug('existingState, so we exit here', new CommunityHandshakeStateLoggingView(pendingState))
|
||||
return
|
||||
}
|
||||
}
|
||||
// load comA and comB parallel
|
||||
// load with joined federated community of given api version
|
||||
const [homeComB, comA] = await Promise.all([
|
||||
getHomeCommunityWithFederatedCommunityOrFail(api),
|
||||
getCommunityByPublicKeyOrFail(publicKey),
|
||||
])
|
||||
// get federated communities with correct api version
|
||||
// simply check and extract federated community from community of given api version or throw error if not found
|
||||
const homeFedComB = getFederatedCommunityWithApiOrFail(homeComB, api)
|
||||
|
||||
// TODO: make sure it is unique
|
||||
const oneTimeCode = randombytes_random().toString()
|
||||
comA.communityUuid = oneTimeCode
|
||||
await DbCommunity.save(comA)
|
||||
methodLogger.debug(
|
||||
`Authentication: stored oneTimeCode in requestedCom:`,
|
||||
new CommunityLoggingView(comA),
|
||||
)
|
||||
const oneTimeCode = randombytes_random()
|
||||
const oneTimeCodeString = oneTimeCode.toString()
|
||||
|
||||
// Create new community handshake state
|
||||
state = new DbCommunityHandshakeState()
|
||||
state.publicKey = publicKey.asBuffer()
|
||||
state.apiVersion = api
|
||||
state.status = CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK
|
||||
state.handshakeId = parseInt(handshakeID)
|
||||
state.oneTimeCode = oneTimeCode
|
||||
state = await state.save()
|
||||
methodLogger.debug('[START_OPEN_CONNECTION_CALLBACK] community handshake state created')
|
||||
|
||||
const client = AuthenticationClientFactory.getInstance(fedComA)
|
||||
|
||||
if (client instanceof V1_0_AuthenticationClient) {
|
||||
const url = homeFedComB.endPoint.endsWith('/')
|
||||
? homeFedComB.endPoint + homeFedComB.apiVersion
|
||||
: homeFedComB.endPoint + '/' + homeFedComB.apiVersion
|
||||
const url = ensureUrlEndsWithSlash(homeFedComB.endPoint) + homeFedComB.apiVersion
|
||||
|
||||
const callbackArgs = new OpenConnectionCallbackJwtPayloadType(handshakeID, oneTimeCode, url)
|
||||
methodLogger.debug(`Authentication: start openConnectionCallback with args:`, callbackArgs)
|
||||
const callbackArgs = new OpenConnectionCallbackJwtPayloadType(handshakeID, oneTimeCodeString, url)
|
||||
// methodLogger.debug(`Authentication: start openConnectionCallback with args:`, callbackArgs)
|
||||
// encrypt callbackArgs with requestedCom.publicJwtKey and sign it with homeCom.privateJwtKey
|
||||
const jwt = await encryptAndSign(callbackArgs, homeComB!.privateJwtKey!, comA.publicJwtKey!)
|
||||
const jwt = await encryptAndSign(callbackArgs, homeComB.privateJwtKey!, comA.publicJwtKey!)
|
||||
const args = new EncryptedTransferArgs()
|
||||
args.publicKey = homeComB!.publicKey.toString('hex')
|
||||
args.publicKey = new Ed25519PublicKey(homeComB.publicKey).asHex()
|
||||
args.jwt = jwt
|
||||
args.handshakeID = handshakeID
|
||||
methodLogger.debug(`invoke openConnectionCallback(), oneTimeCode: ${oneTimeCodeString}`)
|
||||
const result = await client.openConnectionCallback(args)
|
||||
if (result) {
|
||||
methodLogger.debug('startOpenConnectionCallback() successful:', jwt)
|
||||
methodLogger.debug(`startOpenConnectionCallback() successful`)
|
||||
} else {
|
||||
methodLogger.error('startOpenConnectionCallback() failed:', jwt)
|
||||
methodLogger.debug(`jwt: ${jwt}`)
|
||||
const errorString = 'startOpenConnectionCallback() failed'
|
||||
methodLogger.error(errorString)
|
||||
state.status = CommunityHandshakeStateType.FAILED
|
||||
state.lastError = errorString
|
||||
state = await state.save()
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
methodLogger.error('error in startOpenConnectionCallback:', err)
|
||||
methodLogger.error('error in startOpenConnectionCallback', err)
|
||||
if (state) {
|
||||
try {
|
||||
state.status = CommunityHandshakeStateType.FAILED
|
||||
state.lastError = String(err)
|
||||
state = await state.save()
|
||||
} catch(e) {
|
||||
methodLogger.error('error on saving CommunityHandshakeState', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
methodLogger.removeContext('handshakeID')
|
||||
}
|
||||
|
||||
export async function startAuthentication(
|
||||
@ -87,21 +120,31 @@ export async function startAuthentication(
|
||||
oneTimeCode: string,
|
||||
fedComB: DbFedCommunity,
|
||||
): Promise<void> {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity.startAuthentication`)
|
||||
const methodLogger = createLogger('startAuthentication')
|
||||
methodLogger.addContext('handshakeID', handshakeID)
|
||||
methodLogger.debug(`startAuthentication()...`, {
|
||||
oneTimeCode,
|
||||
fedComB: new FederatedCommunityLoggingView(fedComB),
|
||||
})
|
||||
methodLogger.debug(`startAuthentication()... oneTimeCode: ${oneTimeCode}`)
|
||||
let state: DbCommunityHandshakeState | null = null
|
||||
try {
|
||||
const fedComBPublicKey = new Ed25519PublicKey(fedComB.publicKey)
|
||||
const homeComA = await getHomeCommunity()
|
||||
const comB = await DbCommunity.findOneByOrFail({
|
||||
foreign: true,
|
||||
publicKey: fedComB.publicKey,
|
||||
publicKey: fedComBPublicKey.asBuffer(),
|
||||
})
|
||||
if (!comB.publicJwtKey) {
|
||||
throw new Error('Public JWT key still not exist for foreign community')
|
||||
}
|
||||
state = await findPendingCommunityHandshake(fedComBPublicKey, fedComB.apiVersion, CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION)
|
||||
if (!state) {
|
||||
throw new Error('No pending community handshake found')
|
||||
}
|
||||
const stateLogic = new CommunityHandshakeStateLogic(state)
|
||||
if ((await stateLogic.isTimeoutUpdate())) {
|
||||
methodLogger.debug('invalid state', new CommunityHandshakeStateLoggingView(state))
|
||||
throw new Error('No valid pending community handshake found')
|
||||
}
|
||||
state.status = CommunityHandshakeStateType.START_AUTHENTICATION
|
||||
await state.save()
|
||||
|
||||
const client = AuthenticationClientFactory.getInstance(fedComB)
|
||||
|
||||
@ -110,41 +153,55 @@ export async function startAuthentication(
|
||||
// encrypt authenticationArgs.uuid with fedComB.publicJwtKey and sign it with homeCom.privateJwtKey
|
||||
const jwt = await encryptAndSign(authenticationArgs, homeComA!.privateJwtKey!, comB.publicJwtKey!)
|
||||
const args = new EncryptedTransferArgs()
|
||||
args.publicKey = homeComA!.publicKey.toString('hex')
|
||||
args.publicKey = new Ed25519PublicKey(homeComA!.publicKey).asHex()
|
||||
args.jwt = jwt
|
||||
args.handshakeID = handshakeID
|
||||
methodLogger.debug(`invoke authenticate() with:`, args)
|
||||
methodLogger.debug(`invoke authenticate(), publicKey: ${args.publicKey}`)
|
||||
const responseJwt = await client.authenticate(args)
|
||||
methodLogger.debug(`response of authenticate():`, responseJwt)
|
||||
// methodLogger.debug(`response of authenticate():`, responseJwt)
|
||||
|
||||
if (responseJwt !== null) {
|
||||
const payload = await verifyAndDecrypt(handshakeID, responseJwt, homeComA!.privateJwtKey!, comB.publicJwtKey!) as AuthenticationResponseJwtPayloadType
|
||||
methodLogger.debug(
|
||||
/*methodLogger.debug(
|
||||
`received payload from authenticate ComB:`,
|
||||
payload,
|
||||
new FederatedCommunityLoggingView(fedComB),
|
||||
)
|
||||
)*/
|
||||
if (payload.tokentype !== AuthenticationResponseJwtPayloadType.AUTHENTICATION_RESPONSE_TYPE) {
|
||||
const errmsg = `Invalid tokentype in authenticate-response of community with publicKey` + comB.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
throw new Error(`Invalid tokentype in authenticate-response of community with publicKey ${fedComBPublicKey.asHex()}`)
|
||||
}
|
||||
if (!payload.uuid || !validateUUID(payload.uuid) || versionUUID(payload.uuid) !== 4) {
|
||||
const errmsg = `Invalid uuid in authenticate-response of community with publicKey` + comB.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
const parsedUuidv4 = uuidv4Schema.safeParse(payload.uuid)
|
||||
if (!parsedUuidv4.success) {
|
||||
throw new Error(`Invalid uuid in authenticate-response of community with publicKey ${fedComBPublicKey.asHex()}`)
|
||||
}
|
||||
comB.communityUuid = payload.uuid
|
||||
methodLogger.debug('received uuid from authenticate ComB:', parsedUuidv4.data)
|
||||
comB.communityUuid = parsedUuidv4.data
|
||||
comB.authenticatedAt = new Date()
|
||||
await DbCommunity.save(comB)
|
||||
methodLogger.debug('Community Authentication successful:', new CommunityLoggingView(comB))
|
||||
await DbCommunity.save(comB)
|
||||
state.status = CommunityHandshakeStateType.SUCCESS
|
||||
await state.save()
|
||||
methodLogger.debug('[SUCCESS] community handshake state updated')
|
||||
const endTime = new Date()
|
||||
const duration = endTime.getTime() - state.createdAt.getTime()
|
||||
methodLogger.debug(`Community Authentication successful in ${duration} ms`)
|
||||
} else {
|
||||
state.status = CommunityHandshakeStateType.FAILED
|
||||
state.lastError = 'Community Authentication failed, empty response'
|
||||
await state.save()
|
||||
methodLogger.error('Community Authentication failed:', authenticationArgs)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
methodLogger.error('error in startAuthentication:', err)
|
||||
if (state) {
|
||||
try {
|
||||
state.status = CommunityHandshakeStateType.FAILED
|
||||
state.lastError = String(err)
|
||||
await state.save()
|
||||
} catch(e) {
|
||||
methodLogger.error('error on saving CommunityHandshakeState', e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
methodLogger.removeContext('handshakeID')
|
||||
}
|
||||
|
||||
@ -6,8 +6,10 @@ import { getLogger } from 'log4js'
|
||||
// config
|
||||
import { CONFIG } from './config'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from './config/const'
|
||||
import { onShutdown, printServerCrashAsciiArt, ShutdownReason } from 'shared'
|
||||
|
||||
async function main() {
|
||||
const startTime = new Date()
|
||||
// init logger
|
||||
const log4jsConfigFileName = CONFIG.LOG4JS_CONFIG_PLACEHOLDER.replace('%v', CONFIG.FEDERATION_API)
|
||||
initLogger(
|
||||
@ -27,6 +29,16 @@ async function main() {
|
||||
`GraphIQL available at ${CONFIG.FEDERATION_COMMUNITY_URL}/api/${CONFIG.FEDERATION_API}`,
|
||||
)
|
||||
}
|
||||
onShutdown(async (reason, error) => {
|
||||
if (ShutdownReason.SIGINT === reason || ShutdownReason.SIGTERM === reason) {
|
||||
logger.info(`graceful shutdown: ${reason}`)
|
||||
} else {
|
||||
const endTime = new Date()
|
||||
const duration = endTime.getTime() - startTime.getTime()
|
||||
printServerCrashAsciiArt('Server Crash', `reason: ${reason}`, `server was ${duration}ms online`)
|
||||
logger.error(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "2.6.1",
|
||||
"version": "2.7.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "concurrently \"yarn watch-scss\" \"vite\"",
|
||||
@ -80,10 +80,11 @@
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"chokidar-cli": "^3.0.0",
|
||||
"chokidar": "^4.0.3",
|
||||
"concurrently": "^9.1.2",
|
||||
"config-schema": "*",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^10.0.0",
|
||||
"dotenv-webpack": "^7.0.3",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido",
|
||||
"version": "2.6.1",
|
||||
"version": "2.7.0",
|
||||
"description": "Gradido",
|
||||
"main": "index.js",
|
||||
"repository": "git@github.com:gradido/gradido.git",
|
||||
@ -21,7 +21,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"release": "bumpp --no-commit --no-push -r",
|
||||
"version": "auto-changelog -p && git add CHANGELOG.md",
|
||||
"version": "auto-changelog -p --commit-limit 0 && git add CHANGELOG.md",
|
||||
"installAll": "bun run install",
|
||||
"docker": "cross-env BUILD_COMMIT=$(git rev-parse HEAD) docker compose -f docker-compose.yml up",
|
||||
"docker:rebuild": "cross-env BUILD_COMMIT=$(git rev-parse HEAD) docker compose -f docker-compose.yml build",
|
||||
@ -38,7 +38,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.0",
|
||||
"@types/minimatch": "6.0.0"
|
||||
"@types/minimatch": "6.0.0",
|
||||
"bbump": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "shared",
|
||||
"version": "2.6.1",
|
||||
"version": "2.7.0",
|
||||
"description": "Gradido Shared Code, Low-Level Shared Code, without dependencies on other modules",
|
||||
"main": "./build/index.js",
|
||||
"types": "./src/index.ts",
|
||||
@ -37,6 +37,7 @@
|
||||
"esbuild": "^0.25.2",
|
||||
"jose": "^4.14.4",
|
||||
"log4js": "^6.9.1",
|
||||
"yoctocolors-cjs": "^2.1.2",
|
||||
"zod": "^3.25.61"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
export const DECAY_START_TIME = new Date('2021-05-13T17:46:31Z')
|
||||
export const SECONDS_PER_YEAR_GREGORIAN_CALENDER = 31556952.0
|
||||
export const LOG4JS_BASE_CATEGORY_NAME = 'shared'
|
||||
export const REDEEM_JWT_TOKEN_EXPIRATION = '10m'
|
||||
export const REDEEM_JWT_TOKEN_EXPIRATION = '10m'
|
||||
// 10 minutes
|
||||
export const FEDERATION_AUTHENTICATION_TIMEOUT_MS = 60 * 1000 * 10
|
||||
51
shared/src/helper/BinaryData.ts
Normal file
51
shared/src/helper/BinaryData.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { getLogger } from 'log4js'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '../const'
|
||||
|
||||
const logging = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.helper.BinaryData`)
|
||||
|
||||
/**
|
||||
* Class mainly for handling ed25519 public keys,
|
||||
* to make sure we have always the correct Format (Buffer or Hex String)
|
||||
*/
|
||||
export class BinaryData {
|
||||
private buf: Buffer
|
||||
private hex: string
|
||||
|
||||
constructor(input: Buffer | string | undefined) {
|
||||
if (typeof input === 'string') {
|
||||
this.buf = Buffer.from(input, 'hex')
|
||||
this.hex = input
|
||||
} else if (Buffer.isBuffer(input)) {
|
||||
this.buf = input
|
||||
this.hex = input.toString('hex')
|
||||
} else {
|
||||
this.buf = Buffer.from('')
|
||||
this.hex = ''
|
||||
}
|
||||
}
|
||||
|
||||
asBuffer(): Buffer {
|
||||
return this.buf
|
||||
}
|
||||
|
||||
asHex(): string {
|
||||
return this.hex
|
||||
}
|
||||
|
||||
isSame(other: BinaryData): boolean {
|
||||
if (other === undefined || !(other instanceof BinaryData)) {
|
||||
logging.error('other is invalid', other)
|
||||
return false
|
||||
}
|
||||
return this.buf.compare(other.asBuffer()) === 0
|
||||
}
|
||||
}
|
||||
|
||||
export class Ed25519PublicKey extends BinaryData {
|
||||
constructor(input: Buffer | string | undefined) {
|
||||
super(input)
|
||||
if (this.asBuffer().length !== 32) {
|
||||
throw new Error('Invalid ed25519 public key length')
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1 +1,3 @@
|
||||
export * from './updateField'
|
||||
export * from './updateField'
|
||||
export * from './BinaryData'
|
||||
export * from './onShutdown'
|
||||
51
shared/src/helper/onShutdown.ts
Normal file
51
shared/src/helper/onShutdown.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { Logger } from 'log4js'
|
||||
import colors from 'yoctocolors-cjs'
|
||||
|
||||
export enum ShutdownReason {
|
||||
SIGINT = 'SIGINT',
|
||||
SIGTERM = 'SIGTERM',
|
||||
UNCAUGHT_EXCEPTION = 'UNCAUGHT_EXCEPTION',
|
||||
UNCAUGHT_REJECTION = 'UNCAUGHT_REJECTION',
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setup graceful shutdown for the process
|
||||
* @param gracefulShutdown will be called if process is terminated
|
||||
*/
|
||||
export function onShutdown(shutdownHandler: (reason: ShutdownReason, error?: Error | any) => Promise<void>) {
|
||||
const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM']
|
||||
signals.forEach(sig => {
|
||||
process.on(sig, async () => {
|
||||
await shutdownHandler(sig as ShutdownReason)
|
||||
process.exit(0)
|
||||
})
|
||||
})
|
||||
|
||||
process.on('uncaughtException', async (err) => {
|
||||
await shutdownHandler(ShutdownReason.UNCAUGHT_EXCEPTION, err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', async (err) => {
|
||||
await shutdownHandler(ShutdownReason.UNCAUGHT_REJECTION, err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
if (process.platform === "win32") {
|
||||
const rl = require("readline").createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
})
|
||||
rl.on("SIGINT", () => {
|
||||
process.emit("SIGINT" as any)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function printServerCrashAsciiArt(msg1: string, msg2: string, msg3: string) {
|
||||
console.error(colors.redBright(` /\\_/\\ ${msg1}`))
|
||||
console.error(colors.redBright(`( x.x ) ${msg2}`))
|
||||
console.error(colors.redBright(` > < ${msg3}`))
|
||||
console.error(colors.redBright(''))
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
export * from './schema'
|
||||
export * from './enum'
|
||||
export * from './const'
|
||||
export * from './helper'
|
||||
export * from './logic/decay'
|
||||
export * from './jwt/JWT'
|
||||
|
||||
@ -43,11 +43,9 @@ export const verify = async (handshakeID: string, token: string, publicKey: stri
|
||||
})
|
||||
payload.handshakeID = handshakeID
|
||||
methodLogger.debug('verify after jwtVerify... payload=', payload)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return payload as JwtPayloadType
|
||||
} catch (err) {
|
||||
methodLogger.error('verify after jwtVerify... error=', err)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return null
|
||||
}
|
||||
}
|
||||
@ -74,11 +72,9 @@ export const encode = async (payload: JwtPayloadType, privatekey: string): Promi
|
||||
.setExpirationTime(payload.expiration)
|
||||
.sign(secret)
|
||||
methodLogger.debug('encode... token=', token)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return token
|
||||
} catch (e) {
|
||||
methodLogger.error('Failed to sign JWT:', e)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@ -111,11 +107,9 @@ export const encrypt = async (payload: JwtPayloadType, publicKey: string): Promi
|
||||
.setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM' })
|
||||
.encrypt(recipientKey)
|
||||
methodLogger.debug('encrypt... jwe=', jwe)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return jwe.toString()
|
||||
} catch (e) {
|
||||
methodLogger.error('Failed to encrypt JWT:', e)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@ -131,11 +125,9 @@ export const decrypt = async(handshakeID: string, jwe: string, privateKey: strin
|
||||
await compactDecrypt(jwe, decryptKey)
|
||||
methodLogger.debug('decrypt... plaintext=', plaintext)
|
||||
methodLogger.debug('decrypt... protectedHeader=', protectedHeader)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return new TextDecoder().decode(plaintext)
|
||||
} catch (e) {
|
||||
methodLogger.error('Failed to decrypt JWT:', e)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@ -147,7 +139,6 @@ export const encryptAndSign = async (payload: JwtPayloadType, privateKey: string
|
||||
methodLogger.debug('encryptAndSign... jwe=', jwe)
|
||||
const jws = await encode(new EncryptedJWEJwtPayloadType(payload.handshakeID, jwe), privateKey)
|
||||
methodLogger.debug('encryptAndSign... jws=', jws)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return jws
|
||||
}
|
||||
|
||||
@ -171,6 +162,5 @@ export const verifyAndDecrypt = async (handshakeID: string, token: string, priva
|
||||
methodLogger.debug('verifyAndDecrypt... jwe=', jwe)
|
||||
const payload = await decrypt(handshakeID, jwe as string, privateKey)
|
||||
methodLogger.debug('verifyAndDecrypt... payload=', payload)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return JSON.parse(payload) as JwtPayloadType
|
||||
}
|
||||
|
||||
@ -4,4 +4,4 @@ import { validate, version } from 'uuid'
|
||||
export const uuidv4Schema = string().refine((val: string) => validate(val) && version(val) === 4, 'Invalid uuid')
|
||||
export const emailSchema = string().email()
|
||||
export const urlSchema = string().url()
|
||||
export const uint32Schema = number().positive().lte(4294967295)
|
||||
export const uint32Schema = number().positive().lte(4294967295)
|
||||
|
||||
35
shared/src/schema/community.schema.test.ts
Normal file
35
shared/src/schema/community.schema.test.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { communityAuthenticatedSchema } from './community.schema'
|
||||
import { describe, it, expect } from 'bun:test'
|
||||
|
||||
|
||||
describe('communityAuthenticatedSchema', () => {
|
||||
it('should return an error if communityUuid is not a uuidv4', () => {
|
||||
const data = communityAuthenticatedSchema.safeParse({
|
||||
communityUuid: '1234567890',
|
||||
authenticatedAt: new Date(),
|
||||
})
|
||||
|
||||
expect(data.success).toBe(false)
|
||||
expect(data.error?.issues[0].path).toEqual(['communityUuid'])
|
||||
})
|
||||
|
||||
it('should return an error if authenticatedAt is not a date', () => {
|
||||
const data = communityAuthenticatedSchema.safeParse({
|
||||
communityUuid: uuidv4(),
|
||||
authenticatedAt: '2022-01-01',
|
||||
})
|
||||
|
||||
expect(data.success).toBe(false)
|
||||
expect(data.error?.issues[0].path).toEqual(['authenticatedAt'])
|
||||
})
|
||||
|
||||
it('should return no error for valid data and valid uuid4', () => {
|
||||
const data = communityAuthenticatedSchema.safeParse({
|
||||
communityUuid: uuidv4(),
|
||||
authenticatedAt: new Date(),
|
||||
})
|
||||
|
||||
expect(data.success).toBe(true)
|
||||
})
|
||||
})
|
||||
7
shared/src/schema/community.schema.ts
Normal file
7
shared/src/schema/community.schema.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { object, date, array, string } from 'zod'
|
||||
import { uuidv4Schema } from './base.schema'
|
||||
|
||||
export const communityAuthenticatedSchema = object({
|
||||
communityUuid: uuidv4Schema,
|
||||
authenticatedAt: date(),
|
||||
})
|
||||
@ -1,2 +1,3 @@
|
||||
export * from './user.schema'
|
||||
export * from './base.schema'
|
||||
export * from './base.schema'
|
||||
export * from './community.schema'
|
||||
Loading…
x
Reference in New Issue
Block a user