Merge branch 'master' into fix_small_bugs

This commit is contained in:
einhornimmond 2025-11-03 14:04:40 +01:00 committed by GitHub
commit 3fa05cf2c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
85 changed files with 1964 additions and 14018 deletions

1
.bun-version Normal file
View File

@ -0,0 +1 @@
1.3.0

View File

@ -403,10 +403,8 @@ jobs:
##########################################################################
# Push release tag to GitHub #############################################
##########################################################################
- name: yarn install
run: yarn install
- name: generate changelog
run: yarn auto-changelog --commit-limit 0 --latest-version ${{ env.VERSION }} --unreleased-only
run: npx auto-changelog --commit-limit 0 --latest-version ${{ env.VERSION }} --unreleased-only
- name: package-version-to-git-release
continue-on-error: true # Will fail if tag exists
id: create_release

View File

@ -54,6 +54,8 @@ jobs:
- name: install bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: '.bun-version'
- name: install dependencies
run: |
@ -93,6 +95,9 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: install bun
uses: oven-sh/setup-bun@v2
- name: Admin Interface | Locales
run: cd admin && yarn locales
run: cd admin && bun locales

View File

@ -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: |
@ -98,6 +102,9 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: install bun
uses: oven-sh/setup-bun@v2
- name: Backend | Locales
run: cd backend && yarn locales
run: cd backend && bun locales

View File

@ -28,16 +28,18 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
- 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
- name: typecheck
run: cd config-schema && yarn typecheck
run: cd config-schema && bun run typecheck
- name: unit tests
run: cd config-schema && yarn test
run: cd config-schema && bun run test

View File

@ -29,9 +29,11 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: install bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: '.bun-version'
- name: install dependencies
run: |

View File

@ -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,6 +78,8 @@ jobs:
- name: install bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: '.bun-version'
- name: install dependencies
run: |

View File

@ -53,6 +53,8 @@ jobs:
- name: install bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: '.bun-version'
- name: install dependencies
run: |

View File

@ -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
@ -70,7 +72,7 @@ jobs:
id: e2e-tests
run: |
cd e2e-tests/
yarn run cypress run
bun cypress run
- name: End-to-end tests | if tests failed, compile html report
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
@ -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

View File

@ -53,6 +53,8 @@ jobs:
- name: install bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: '.bun-version'
- name: install dependencies
run: |

View File

@ -52,12 +52,14 @@ 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
- name: Frontend | Unit tests
run: cd frontend && yarn test
run: cd frontend && bun run test
lint:
if: needs.files-changed.outputs.config == 'true' || needs.files-changed.outputs.frontend == 'true'
@ -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
@ -106,6 +110,9 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: install bun
uses: oven-sh/setup-bun@v2
- name: Frontend | Locales
run: cd frontend && yarn locales
run: cd frontend && bun run locales

View File

@ -30,13 +30,15 @@ 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
- name: typecheck
run: cd shared && yarn typecheck
run: cd shared && bun run typecheck
- name: unit tests
run: cd shared && yarn test
run: cd shared && bun run test

12
.vscode/launch.json vendored
View File

@ -9,7 +9,7 @@
"request": "launch",
"name": "Database Debug Tests",
"stopOnEntry": true,
"runtimeExecutable": "yarn",
"runtimeExecutable": "bun",
"runtimeArgs": [
"run",
"test"
@ -25,7 +25,7 @@
"type": "node",
"request": "launch",
"name": "DHT-Node Debug Tests",
"runtimeExecutable": "yarn",
"runtimeExecutable": "bun",
"runtimeArgs": [
"run",
"test:debug"
@ -42,7 +42,7 @@
"request": "launch",
"name": "DHT-Node Debug",
"stopOnEntry": true,
"runtimeExecutable": "yarn",
"runtimeExecutable": "bun",
"runtimeArgs": [
"run",
"dev"
@ -58,7 +58,7 @@
"type": "node",
"request": "launch",
"name": "Federation Debug Tests",
"runtimeExecutable": "yarn",
"runtimeExecutable": "bun",
"runtimeArgs": [
"run",
"test:debug"
@ -75,7 +75,7 @@
"request": "launch",
"name": "Federation Debug",
"stopOnEntry": true,
"runtimeExecutable": "yarn",
"runtimeExecutable": "bun",
"runtimeArgs": [
"run",
"dev"
@ -92,7 +92,7 @@
"request": "launch",
"name": "Backend Debug",
"stopOnEntry": true,
"runtimeExecutable": "yarn",
"runtimeExecutable": "bun",
"runtimeArgs": [
"run",
"dev"

View File

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

View File

@ -48,8 +48,6 @@ RUN bun install --global turbo
# Add bun's global bin directory to PATH
ENV PATH="/root/.bun/bin:${PATH}"
#RUN yarn global add turbo
# Settings
## Expose Container Port
EXPOSE ${BACKEND_PORT}
@ -85,10 +83,6 @@ COPY --chown=app:app ./ ./
# yarn install
RUN bun install --frozen-lockfile --non-interactive
# try with bun, use yarn if problems occur
# go into admin folder and use yarn to install local dependencies which need to use nohoist for @vee-validate/i18n which isn't supported by bun
#RUN bun install --frozen-lockfile
##################################################################################
# TEST ###########################################################################
@ -136,7 +130,7 @@ WORKDIR ${DOCKER_WORKDIR}
# Copy only the build artifacts from the previous build stage
COPY --chown=app:app --from=build /app/node_modules ./node_modules
COPY --chown=app:app --from=build /app/package.json ./package.json
COPY --chown=app:app --from=build /app/yarn.lock ./yarn.lock
COPY --chown=app:app --from=build /app/bun.lock ./bun.lock
COPY --chown=app:app --from=build /app/turbo.json ./turbo.json
# and Turbo cache to prevent rebuilding
COPY --chown=app:app --from=build /tmp/turbo ./tmp/turbo

View File

@ -88,13 +88,6 @@ bun install
bun install --global turbo@^2
```
If this does not work, try to use [yarn](https://classic.yarnpkg.com/en/docs/install) instead
```bash
yarn install
yarn global add turbo@^2
```
- **Development Mode (Hot-Reload)**:
Launches Gradido with hot-reloading for fast iteration.
@ -124,10 +117,6 @@ The installation of dockers depends on your selected product package from the [d
* In case the docker desktop will not start correctly because of previous docker installations, then please clean the used directories of previous docker installation - `C:\Users` - before you retry starting docker desktop. For further problems executing docker desktop please take a look in this description "[logs and trouble shooting](https://docs.docker.com/desktop/windows/troubleshoot/)"
* In case your docker desktop installation causes high memory consumption per vmmem process, then please take a look at this description "[vmmen process consuming too much memory (Docker Desktop)](https://dev.to/tallesl/vmmen-process-consuming-too-much-memory-docker-desktop-273p)"
### yarn
For the Gradido build process the yarn package manager will be used. Please download and install [yarn for windows](https://phoenixnap.com/kb/yarn-windows) by following the instructions there.
### ⚡ Workspaces and Bun Compatibility
The project now uses **Workspaces**, and work is ongoing to make all modules **Bun-compatible**. You can currently use `bun install`, but not all modules are fully Bun-compatible yet.
@ -143,12 +132,10 @@ To install dependencies with Bun:
bun install
```
Note that some modules are still not fully compatible with Bun. Therefore, continue using **Yarn** for development if you run into any issues.
### EMFILE: too many open files
With
```bash
yarn docker_dev
bun docker_dev
```
or also
```bash
@ -161,11 +148,11 @@ which you are working on in dev mode and the rest in production mode.
For example if you are only working on the frontend, you can start the frontend in dev mode and the rest in production mode:
```bash
yarn docker_dev frontend
bun docker_dev frontend
```
and in another bash
```bash
yarn docker backend admin database nginx --no-deps
bun docker backend admin database nginx --no-deps
```
or local with turbo
```bash
@ -218,10 +205,10 @@ Currently Modules `frontend`, `admin`, `share` and `core` running the tests in p
`database`, `backend`, `dht-node` and `federation` are running the tests still serially.
### Clear
In root folder calling `yarn clear` will clear all turbo caches, node_modules and build folders of all workspaces for a clean rebuild.
In root folder calling `bun clear` will clear all turbo caches, node_modules and build folders of all workspaces for a clean rebuild.
```bash
yarn clear
bun clear
```
@ -254,13 +241,13 @@ To generate the Changelog and set a new Version you should use the following com
```bash
git fetch --all
yarn release
bun release
```
The first command `git fetch --all` will make sure you have all tags previously defined which is required to generate a correct changelog. The second command `yarn release` will execute the changelog tool and set version numbers in the main package and sub-packages. It is required to do `yarn install` before you can use this command.
The first command `git fetch --all` will make sure you have all tags previously defined which is required to generate a correct changelog. The second command `bun release` will execute the changelog tool and set version numbers in the main package and sub-packages. It is required to do `bun install` before you can use this command.
After generating a new version you should commit the changes. This will be the CHANGELOG.md and several package.json files. This commit will be omitted in the changelog.
Note: The Changelog will be regenerated with all tags on release on the external builder tool, but will not be checked in there. The Changelog on the github release will therefore always be correct, on the repo it might be incorrect due to missing tags when executing the `yarn release` command.
Note: The Changelog will be regenerated with all tags on release on the external builder tool, but will not be checked in there. The Changelog on the github release will therefore always be correct, on the repo it might be incorrect due to missing tags when executing the `bun release` command.
## How the different .env work on deploy

View File

@ -57,7 +57,9 @@ WORKDIR ${DOCKER_WORKDIR}
FROM base as bun-base
RUN apk update && apk add --no-cache curl tar bash
RUN curl -fsSL https://bun.sh/install | bash
COPY .bun-version .bun-version
RUN BUN_VERSION=$(cat .bun-version) && \
curl -fsSL https://bun.com/install | bash -s "bun-v${BUN_VERSION}"
# Add bun's global bin directory to PATH
ENV PATH="/root/.bun/bin:${PATH}"

View File

@ -1,26 +1,48 @@
# admin
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn serve
turbo dev
```
or from root folder:
```
turbo admin#dev
```
### Compiles and minifies for production
```
yarn build
turbo build
```
or from root folder:
```
turbo admin#build
```
### Lints and fixes files
```
yarn lint
turbo lint
```
or from root folder:
```
turbo admin#lint
```
### Unit tests
```
yarn test
turbo test
```
For filtering out single tests:
```
turbo test -- <test_name>
```
Everything after -- will be passed to vitest.
or from root folder:
```
turbo admin#test
```

View File

@ -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",
@ -71,7 +72,7 @@
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-vue": "8.7.1",
"joi": "^17.13.3",
"joi": "17.13.3",
"jsdom": "^25.0.0",
"lightningcss": "^1.30.1",
"mock-apollo-client": "^1.2.1",

View File

@ -54,8 +54,9 @@ WORKDIR ${DOCKER_WORKDIR}
FROM base as bun-base
RUN apt update && apt install -y --no-install-recommends ca-certificates curl bash unzip
#RUN apk update && apk add --no-cache curl tar bash
RUN curl -fsSL https://bun.sh/install | bash
COPY .bun-version .bun-version
RUN BUN_VERSION=$(cat .bun-version) && \
curl -fsSL https://bun.com/install | bash -s "bun-v${BUN_VERSION}"
# Add bun's global bin directory to PATH
ENV PATH="/root/.bun/bin:${PATH}"

View File

@ -5,7 +5,7 @@
## Seed DB
```bash
yarn seed
turbo seed
```
Deletes all data in database. Then seeds data in database.

View File

@ -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",
@ -75,7 +76,7 @@
"helmet": "^5.1.1",
"i18n": "^0.15.1",
"jest": "27.2.4",
"joi": "^17.13.3",
"joi": "17.13.3",
"jose": "^4.14.4",
"klicktipp-api": "^1.0.2",
"lodash.clonedeep": "^4.5.0",

View File

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

View File

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

View File

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

View File

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

1344
bun.lock

File diff suppressed because it is too large Load Diff

2
bunfig.toml Normal file
View File

@ -0,0 +1,2 @@
[install]
linker = "hoisted"

View File

@ -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",
@ -32,7 +32,7 @@
},
"dependencies": {
"esbuild": "^0.25.2",
"joi": "^17.13.3",
"joi": "17.13.3",
"log4js": "^6.9.1",
"source-map-support": "^0.5.21",
"yoctocolors-cjs": "^2.1.2",

View File

@ -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": "*",
@ -37,10 +38,12 @@
"devDependencies": {
"@biomejs/biome": "2.0.0",
"@types/i18n": "^0.13.4",
"@types/minimatch": "6.0.0",
"@types/node": "^17.0.21",
"@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",

View File

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

View File

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

View File

@ -22,4 +22,5 @@ export * from './util/calculateSenderBalance'
export * from './util/utilities'
export * from './validation/user'
export * from './config/index'
export * from './logic'

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

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

View 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
View File

@ -0,0 +1,2 @@
export * from './CommunityHandshakeState.logic'
export * from './community.logic'

View File

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

View File

@ -47,7 +47,8 @@
// "baseUrl": ".", /* Base directory to resolve non-absolute module names. */
// "paths": { }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [".", "../database"], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
/* List of folders to include type definitions from. */
"typeRoots": ["./node_modules/@types", "../node_modules/@types"],
// "types": ["bun-types"], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */

View File

@ -46,7 +46,9 @@ FROM base as bun-base
#RUN apt update && apt install -y --no-install-recommends ca-certificates curl bash unzip
RUN apk update && apk add --no-cache curl tar bash
RUN curl -fsSL https://bun.sh/install | bash
COPY .bun-version .bun-version
RUN BUN_VERSION=$(cat .bun-version) && \
curl -fsSL https://bun.com/install | bash -s "bun-v${BUN_VERSION}"
# Add bun's global bin directory to PATH
ENV PATH="/root/.bun/bin:${PATH}"
@ -63,7 +65,7 @@ COPY --chown=app:app . .
FROM installer as build-shared
RUN bun install --filter shared --no-cache --frozen-lockfile \
&& cd shared && yarn typecheck && yarn build
&& cd shared && bun run typecheck && bun run build
##################################################################################
# Build ##########################################################################
@ -75,7 +77,7 @@ RUN bun install --filter database --production --no-cache --frozen-lockfile
##################################################################################
# PRODUCTION IMAGE ###############################################################
##################################################################################
FROM base as production
FROM bun-base as production
COPY --chown=app:app --from=build-shared ${DOCKER_WORKDIR}/shared/build ./shared/build
COPY --chown=app:app --from=build-shared ${DOCKER_WORKDIR}/shared/package.json ./shared/package.json
@ -89,7 +91,7 @@ COPY --chown=app:app --from=build ${DOCKER_WORKDIR}/package.json ./package.json
FROM production as up
# Run command
CMD /bin/sh -c "cd database && yarn up"
CMD /bin/sh -c "cd database && bun run up"
##################################################################################
# TEST RESET #####################################################################
@ -97,7 +99,7 @@ CMD /bin/sh -c "cd database && yarn up"
FROM production as reset
# Run command
CMD /bin/sh -c "cd database && yarn reset"
CMD /bin/sh -c "cd database && bun run reset"
##################################################################################
# TEST DOWN ######################################################################
@ -105,4 +107,4 @@ CMD /bin/sh -c "cd database && yarn reset"
FROM production as down
# Run command
CMD /bin/sh -c "cd database && yarn down"
CMD /bin/sh -c "cd database && bun run down"

View File

@ -23,20 +23,20 @@ TypeError: undefined is not an object (evaluating 'module.parent.parent.require'
## Upgrade migrations
```bash
yarn up
turbo up
```
## Downgrade migrations
```bash
yarn down
turbo down
```
## Reset database
```bash
yarn reset
turbo reset
```
Runs all down migrations and after this all up migrations.
@ -45,7 +45,7 @@ Runs all down migrations and after this all up migrations.
call truncate for all tables
```bash
yarn clear
turbo clearDB
```

View File

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

View File

@ -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",
@ -56,8 +57,8 @@
"dotenv": "^10.0.0",
"esbuild": "^0.25.2",
"geojson": "^0.5.0",
"joi-extract-type": "^15.0.8",
"log4js": "^6.9.1",
"mysql": "^2.18.1",
"mysql2": "^2.3.0",
"reflect-metadata": "^0.1.13",
"shared": "*",

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

View File

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

View 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'
}

View File

@ -0,0 +1 @@
export * from './CommunityHandshakeStateType'

View File

@ -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 * from './entity'
export * from './logging'
export * from './queries'
export * from './util'
export * from './util'
export * from './enum'
export { AppDatabase } from './AppDatabase'

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

View File

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

View File

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

View File

@ -1,8 +1,9 @@
import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from '..'
import { AppDatabase } from '../AppDatabase'
import { getHomeCommunity, getReachableCommunities } from './communities'
import { getCommunityByPublicKeyOrFail, getHomeCommunity, getHomeCommunityWithFederatedCommunityOrFail, getReachableCommunities } from './communities'
import { describe, expect, it, beforeEach, beforeAll, afterAll } from 'vitest'
import { createCommunity, createVerifiedFederatedCommunity } from '../seeds/community'
import { Ed25519PublicKey } from 'shared'
const db = AppDatabase.getInstance()
@ -39,6 +40,36 @@ describe('community.queries', () => {
expect(community?.privateKey).toStrictEqual(homeCom.privateKey)
})
})
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)

View File

@ -1,6 +1,6 @@
import { FindOptionsOrder, FindOptionsWhere, IsNull, MoreThanOrEqual, Not } from 'typeorm'
import { Community as DbCommunity } from '../entity'
import { urlSchema, uuidv4Schema } from 'shared'
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 },
})
}
@ -42,6 +49,22 @@ export async function getCommunityWithFederatedCommunityByIdentifier(
})
}
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(
@ -60,4 +83,13 @@ export async function getReachableCommunities(
],
order,
})
}
export async function getNotReachableCommunities(
order?: FindOptionsOrder<DbCommunity>
): Promise<DbCommunity[]> {
return await DbCommunity.find({
where: { authenticatedAt: IsNull(), foreign: true },
order,
})
}

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

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

View File

@ -5,5 +5,6 @@ export * from './communities'
export * from './pendingTransactions'
export * from './transactions'
export * from './transactionLinks'
export * from './communityHandshakes'
export const LOG4JS_QUERIES_CATEGORY_NAME = `${LOG4JS_BASE_CATEGORY_NAME}.queries`

View File

@ -2,7 +2,13 @@ import { Community, FederatedCommunity } from '../entity'
import { randomBytes } from 'node:crypto'
import { v4 as uuidv4 } from 'uuid'
export async function createCommunity(foreign: boolean, save: boolean = true): Promise<Community> {
/**
* 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()
@ -23,14 +29,22 @@ export async function createCommunity(foreign: boolean, save: boolean = true): P
community.description = 'HomeCommunity-description'
community.url = 'http://localhost/api'
}
return save ? await community.save() : community
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
store: boolean = true
): Promise<FederatedCommunity> {
const federatedCommunity = new FederatedCommunity()
federatedCommunity.apiVersion = apiVersion
@ -38,5 +52,5 @@ export async function createVerifiedFederatedCommunity(
federatedCommunity.publicKey = community.publicKey
federatedCommunity.community = community
federatedCommunity.verifiedAt = new Date(Date.now() - verifiedBeforeMs)
return save ? await federatedCommunity.save() : federatedCommunity
return store ? await federatedCommunity.save() : federatedCommunity
}

View File

@ -4,6 +4,9 @@
"clear": {
"cache": false
},
"clearDB": {
"cache": false
},
"up:backend_test": {
"cache": false
},

View File

@ -19,19 +19,34 @@ install_nvm() {
}
nvm use || install_nvm
# check for some tools and install them, when missing
# bun https://bun.sh/install, faster packet-manager as yarn
if ! command -v bun &> /dev/null
# unzip needed for bun install script
if ! command -v unzip &> /dev/null
then
if ! command -v unzip &> /dev/null
then
echo "'unzip' is missing, will be installed now!"
sudo apt-get install -y unzip
fi
echo "'bun' is missing, will be installed now!"
curl -fsSL https://bun.sh/install | bash
echo "'unzip' is missing, will be installed now!"
sudo apt-get install -y unzip
fi
# check for some tools and install them, when missing
# bun https://bun.com/install, faster packet-manager as yarn
BUN_VERSION_FILE="$PROJECT_ROOT/.bun-version"
if [ ! -f "$BUN_VERSION_FILE" ]; then
echo ".bun-version file not found at: $BUN_VERSION_FILE"
exit 1
fi
BUN_VERSION="$(cat "$BUN_VERSION_FILE" | tr -d '[:space:]')"
if ! command -v bun &> /dev/null
then
echo "'bun' is missing, v$BUN_VERSION will be installed now!"
curl -fsSL https://bun.com/install | bash -s "bun-v${BUN_VERSION}"
export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$PATH"
else
CURRENT_VERSION="$(bun --version | tr -d '[:space:]')"
if [ "$CURRENT_VERSION" != "$BUN_VERSION" ]
then
echo "'bun' is outdated, v$BUN_VERSION will be installed now!"
curl -fsSL https://bun.com/install | bash -s "bun-v${BUN_VERSION}"
fi
fi
# turbo https://turborepo.com/docs/getting-started
if ! command -v turbo &> /dev/null

View File

@ -41,7 +41,7 @@ mysql -u ${DB_USER} -p${DB_PASSWORD} <<EOFMYSQL
EOFMYSQL
# Update database if needed (use dev_up for seeding setups)
yarn --cwd $PROJECT_ROOT/database up
turbo up
# Start gradido-backend service
pm2 start gradido-backend

View File

@ -57,8 +57,9 @@ WORKDIR ${DOCKER_WORKDIR}
FROM base as bun-base
RUN apt update && apt install -y --no-install-recommends ca-certificates curl bash unzip
#RUN apk update && apk add --no-cache curl tar bash
RUN curl -fsSL https://bun.sh/install | bash
COPY .bun-version .bun-version
RUN BUN_VERSION=$(cat .bun-version) && \
curl -fsSL https://bun.com/install | bash -s "bun-v${BUN_VERSION}"
# Add bun's global bin directory to PATH
ENV PATH="/root/.bun/bin:${PATH}"

View File

@ -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/",
@ -34,15 +34,14 @@
"@swc/jest": "^0.2.38",
"@types/dotenv": "^8.2.3",
"@types/jest": "27.5.1",
"@types/joi": "^17.2.3",
"@types/node": "^17.0.45",
"@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",
"joi": "17.13.3",
"log4js": "^6.9.1",
"nodemon": "^2.0.7",
"prettier": "^2.8.8",

View File

@ -55,8 +55,9 @@ WORKDIR ${DOCKER_WORKDIR}
FROM base as bun-base
RUN apt update && apt install -y --no-install-recommends ca-certificates curl bash unzip
#RUN apk update && apk add --no-cache curl tar bash
RUN curl -fsSL https://bun.sh/install | bash
COPY .bun-version .bun-version
RUN BUN_VERSION=$(cat .bun-version) && \
curl -fsSL https://bun.com/install | bash -s "bun-v${BUN_VERSION}"
# Add bun's global bin directory to PATH
ENV PATH="/root/.bun/bin:${PATH}"

View File

@ -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",
@ -55,14 +57,16 @@
"graphql-tag": "^2.12.6",
"helmet": "^7.1.0",
"jest": "27.2.4",
"joi": "^17.13.3",
"joi": "17.13.3",
"lodash.clonedeep": "^4.5.0",
"log4js": "^6.7.1",
"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",

View File

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

View File

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

View File

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

View File

@ -57,7 +57,9 @@ WORKDIR ${DOCKER_WORKDIR}
FROM base as bun-base
RUN apk update && apk add --no-cache curl tar bash
RUN curl -fsSL https://bun.sh/install | bash
COPY .bun-version .bun-version
RUN BUN_VERSION=$(cat .bun-version) && \
curl -fsSL https://bun.com/install | bash -s "bun-v${BUN_VERSION}"
# Add bun's global bin directory to PATH
ENV PATH="/root/.bun/bin:${PATH}"

View File

@ -7,26 +7,54 @@ Then install grass, it need some time, because it will be compiled.
```bash
cargo install grass
```
Now with using yarn compile-sass or turbo compile-sass grass will be used.
Now with using bun compile-sass or turbo compile-sass grass will be used.
## install mit yarn
```bash
cd frontend
yarn install
yarn run serve
# build
yarn run build
### Compiles and hot-reloads for development
```
turbo dev
```
or from root folder:
```
turbo frontend#dev
```
## install mit docker
```bash
# build
docker build -t gradido-frontend .
### Compiles and minifies for production
```
turbo build
```
or from root folder:
# run
docker run -it -p 80:80 --rm gradido-frontend
```
turbo frontend#build
```
### Lints and fixes files
```
turbo lint
```
or from root folder:
```
turbo frontend#lint
```
### Unit tests
```
turbo test
```
For filtering out single tests:
```
turbo test -- <test_name>
```
Everything after -- will be passed to vitest.
or from root folder:
```
turbo frontend#test
```
**Fully Coded Components**

View File

@ -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",
@ -96,7 +97,7 @@
"eslint-plugin-vitest": "^0.5.4",
"eslint-plugin-vue": "8.7.1",
"eslint-webpack-plugin": "^5.0.0",
"joi": "^17.13.3",
"joi": "17.13.3",
"jsdom": "^25.0.0",
"lightningcss": "^1.30.1",
"mock-apollo-client": "^1.2.1",

View File

@ -1,13 +1,13 @@
{
"name": "gradido",
"version": "2.6.1",
"version": "2.7.0",
"description": "Gradido",
"main": "index.js",
"repository": "git@github.com:gradido/gradido.git",
"author": "Gradido Academy - https://www.gradido.net",
"license": "Apache-2.0",
"private": true,
"packageManager": "yarn@1.22.22",
"packageManager": "bun@1.2.0",
"workspaces": [
"admin",
"backend",
@ -20,8 +20,9 @@
"shared"
],
"scripts": {
"release": "scripts/release.sh",
"installAll": "yarn install",
"release": "bumpp --no-commit --no-push -r",
"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",
"docker_dev": "cross-env BUILD_COMMIT=$(git rev-parse HEAD) docker compose up",
@ -36,7 +37,9 @@
"uuid": "^8.3.2"
},
"devDependencies": {
"@biomejs/biome": "2.0.0"
"@biomejs/biome": "2.0.0",
"@types/minimatch": "6.0.0",
"bbump": "^1.0.2"
},
"engines": {
"node": ">=18"

View File

@ -1,50 +0,0 @@
#!/bin/bash
# find directories
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
PROJECT_DIR="${SCRIPT_DIR}/../"
FRONTEND_DIR="${PROJECT_DIR}/frontend/"
BACKEND_DIR="${PROJECT_DIR}/backend/"
DATABASE_DIR="${PROJECT_DIR}/database/"
SHARED_DIR="${PROJECT_DIR}/shared/"
CONFIG_SCHEMA_DIR="${PROJECT_DIR}/config-schema/"
CORE_DIR="${PROJECT_DIR}/core/"
ADMIN_DIR="${PROJECT_DIR}/admin/"
DHTNODE_DIR="${PROJECT_DIR}/dht-node/"
FEDERATION_DIR="${PROJECT_DIR}/federation/"
DLTCONNECTOR_DIR="${PROJECT_DIR}/dlt-connector/"
# navigate to project directory
cd ${PROJECT_DIR}
# ask for new version
yarn version --no-git-tag-version --no-commit-hooks --no-commit
# find new version
VERSION="$(node -p -e "require('./package.json').version")"
# update version in sub projects
cd ${FRONTEND_DIR}
yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${VERSION}
cd ${BACKEND_DIR}
yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${VERSION}
cd ${DATABASE_DIR}
yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${VERSION}
cd ${SHARED_DIR}
yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${VERSION}
cd ${CONFIG_SCHEMA_DIR}
yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${VERSION}
cd ${CORE_DIR}
yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${VERSION}
cd ${ADMIN_DIR}
yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${VERSION}
cd ${DHTNODE_DIR}
yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${VERSION}
cd ${FEDERATION_DIR}
yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${VERSION}
cd ${DLTCONNECTOR_DIR}
yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${VERSION}
# generate changelog
cd ${PROJECT_DIR}
./node_modules/.bin/auto-changelog --commit-limit 0 --latest-version ${VERSION}

View File

@ -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",
@ -26,6 +26,7 @@
},
"devDependencies": {
"@biomejs/biome": "2.0.0",
"@types/minimatch": "6.0.0",
"@types/node": "^17.0.21",
"@types/uuid": "^10.0.0",
"typescript": "^4.9.5",
@ -36,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": {

View File

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

View 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')
}
}
}

View File

@ -1 +1,3 @@
export * from './updateField'
export * from './updateField'
export * from './BinaryData'
export * from './onShutdown'

View 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(''))
}

View File

@ -1,5 +1,6 @@
export * from './schema'
export * from './enum'
export * from './const'
export * from './helper'
export * from './logic/decay'
export * from './jwt/JWT'

View File

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

View File

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

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

View 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(),
})

View File

@ -1,2 +1,3 @@
export * from './user.schema'
export * from './base.schema'
export * from './base.schema'
export * from './community.schema'

View File

@ -47,7 +47,8 @@
// "baseUrl": ".", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [".", "../database"], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
/* List of folders to include type definitions from. */
"typeRoots": ["./node_modules/@types", "../node_modules/@types"],
// "types": ["bun-types"], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */

12998
yarn.lock

File diff suppressed because it is too large Load Diff