Merge branch 'master' into refactor_dlt_connector_modern_stack

This commit is contained in:
einhornimmond 2025-09-03 14:42:07 +02:00
commit b17c381f6c
100 changed files with 5230 additions and 26070 deletions

View File

@ -10,6 +10,7 @@ jobs:
# JOB: DOCKER BUILD PRODUCTION FRONTEND ######################################
##############################################################################
build_production_frontend:
if: startsWith(github.event.head_commit.message, 'chore(release):')
name: Docker Build Production - Frontend
runs-on: ubuntu-latest
#needs: [nothing]
@ -47,6 +48,7 @@ jobs:
# JOB: DOCKER BUILD PRODUCTION ADMIN #########################################
##############################################################################
build_production_admin:
if: startsWith(github.event.head_commit.message, 'chore(release):')
name: Docker Build Production - Admin
runs-on: ubuntu-latest
#needs: [nothing]
@ -84,6 +86,7 @@ jobs:
# JOB: DOCKER BUILD PRODUCTION BACKEND #######################################
##############################################################################
build_production_backend:
if: startsWith(github.event.head_commit.message, 'chore(release):')
name: Docker Build Production - Backend
runs-on: ubuntu-latest
#needs: [nothing]
@ -121,6 +124,7 @@ jobs:
# JOB: DOCKER BUILD PRODUCTION DHT-NODE ######################################
##############################################################################
build_production_dht-node:
if: startsWith(github.event.head_commit.message, 'chore(release):')
name: Docker Build Production - DHT-Node
runs-on: ubuntu-latest
#needs: [nothing]
@ -158,6 +162,7 @@ jobs:
# JOB: DOCKER BUILD PRODUCTION FEDERATION ######################################
##############################################################################
build_production_federation:
if: startsWith(github.event.head_commit.message, 'chore(release):')
name: Docker Build Production - Federation
runs-on: ubuntu-latest
#needs: [nothing]
@ -195,6 +200,7 @@ jobs:
# JOB: DOCKER BUILD PRODUCTION DATABASE UP ###################################
##############################################################################
build_production_database_up:
if: startsWith(github.event.head_commit.message, 'chore(release):')
name: Docker Build Production - Database up
runs-on: ubuntu-latest
#needs: [nothing]
@ -221,6 +227,7 @@ jobs:
# JOB: DOCKER BUILD PRODUCTION NGINX #########################################
##############################################################################
build_production_nginx:
if: startsWith(github.event.head_commit.message, 'chore(release):')
name: Docker Build Production - Nginx
runs-on: ubuntu-latest
#needs: [nothing]
@ -258,6 +265,7 @@ jobs:
# JOB: UPLOAD TO DOCKERHUB ###################################################
##############################################################################
upload_to_dockerhub:
if: startsWith(github.event.head_commit.message, 'chore(release):')
name: Upload to Dockerhub
runs-on: ubuntu-latest
needs: [build_production_frontend, build_production_backend, build_production_database_up, build_production_nginx]
@ -315,8 +323,6 @@ jobs:
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/database_up.tar
- name: Load Docker Image
run: docker load < /tmp/mariadb.tar
- name: Download Docker Image (Nginx)
uses: actions/download-artifact@v4
with:
@ -348,6 +354,7 @@ jobs:
##############################################################################
##############################################################################
github_tag:
if: startsWith(github.event.head_commit.message, 'chore(release):')
name: Tag latest version on Github
runs-on: ubuntu-latest
needs: [upload_to_dockerhub]

View File

@ -32,7 +32,7 @@ jobs:
uses: actions/checkout@v3
- name: Database | Build image
run: docker build --target build -t "gradido/database:build" -f database/Dockerfile .
run: docker build --target up -t "gradido/database:up" -f database/Dockerfile .
database_migration_test:
if: needs.files-changed.outputs.database == 'true' || needs.files-changed.outputs.docker-compose == 'true' || needs.files-changed.outputs.mariadb == 'true' || needs.files-changed.outputs.shared == 'true'

View File

@ -4,8 +4,23 @@ 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).
#### [2.6.0](https://github.com/gradido/gradido/compare/2.3.1...2.6.0)
#### [2.6.1](https://github.com/gradido/gradido/compare/2.3.1...2.6.1)
- 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)
- feat(backend): introduce encrypted jwts in backend federation communication [`#3510`](https://github.com/gradido/gradido/pull/3510)
- feat(other): write playwright tests [`#3509`](https://github.com/gradido/gradido/pull/3509)
- feat(frontend): keep branding project in browser url bar [`#3512`](https://github.com/gradido/gradido/pull/3512)
- feat(other): add clear command for yarn and turbo [`#3513`](https://github.com/gradido/gradido/pull/3513)
- fix(other): in deploy run only core count tasks with turbo at the same time [`#3511`](https://github.com/gradido/gradido/pull/3511)
- refactor(federation): move code for checking pending transactions [`#3508`](https://github.com/gradido/gradido/pull/3508)
- refactor(other): add shared module [`#3507`](https://github.com/gradido/gradido/pull/3507)
- refactor(other): centralize logging code, use log4js config-generator [`#3506`](https://github.com/gradido/gradido/pull/3506)
- fix(frontend): fix password labels [`#3504`](https://github.com/gradido/gradido/pull/3504)
- fix(backend): update contribution frontend link [`#3502`](https://github.com/gradido/gradido/pull/3502)
- refactor(database): move database connection into database module [`#3503`](https://github.com/gradido/gradido/pull/3503)
- chore(release): v2.6.0 beta [`#3501`](https://github.com/gradido/gradido/pull/3501)
- fix(frontend): fix contribution link [`#3500`](https://github.com/gradido/gradido/pull/3500)
- feat(other): disable index html caching, reenable limits [`#3497`](https://github.com/gradido/gradido/pull/3497)
- fix(admin): fix accidently remove user states [`#3496`](https://github.com/gradido/gradido/pull/3496)

View File

@ -174,6 +174,49 @@ turbo frontend#dev backend#start admin#start --env-mode=loose
Tip: for local setup use a local nginx server with similar config like docker nginx [nginx.conf](./nginx/gradido.conf) but replace docker image name with localhost
### Testing
This project uses a mocked `log4js` logger for tests.
- **clearLogs()**: Clears all collected logs. Call in `beforeEach` to start with a clean slate.
- **printLogs()**: Prints all collected logs since the last call to clearLogs for debugging purposes.
- Supports log levels: `trace`, `debug`, `info`, `warn`, `error`, `fatal`.
- Logs include context and additional arguments.
- Example:
```ts
import { clearLogs, printLogs } from 'config-schema/test/testSetup'
beforeEach(() => {
clearLogs()
})
describe('test', () => {
it('test', () => {
expect(functionCall()).toBe(true)
printLogs()
})
})
```
#### Include Paths by test framework:
- jest (backend, dht-node, federation):
```ts
import { clearLogs, printLogs } from 'config-schema/test/testSetup'
```
- vitest (frontend, admin, database):
```ts
import { clearLogs, printLogs } from 'config-schema/test/testSetup.vitest'
```
- bun (shared, core):
```ts
import { clearLogs, printLogs } from 'config-schema/test/testSetup.bun'
```
#### Attention!
It isn't tested for parallel running tests yet.
Currently Modules `frontend`, `admin`, `share` and `core` running the tests in parallel,
`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.

View File

@ -3,7 +3,7 @@
"description": "Administration Interface for Gradido",
"main": "index.js",
"author": "Gradido Academy - https://www.gradido.net",
"version": "2.6.0",
"version": "2.6.1",
"license": "Apache-2.0",
"scripts": {
"dev": "vite",

View File

@ -28,6 +28,9 @@
<BListGroupItem>
{{ $t('federation.publicKey') }}&nbsp;{{ item.publicKey }}
</BListGroupItem>
<BListGroupItem v-if="item.hieroTopicId && item.foreign">
{{ $t('federation.hieroTopicId') }}&nbsp;{{ item.hieroTopicId }}
</BListGroupItem>
<BListGroupItem v-if="!item.foreign">
<editable-group
:allow-edit="$store.state.moderator.roles.includes('ADMIN')"
@ -39,6 +42,10 @@
<p style="text-wrap: nowrap">{{ $t('federation.gmsApiKey') }}&nbsp;</p>
<span class="d-block" style="overflow-x: auto">{{ gmsApiKey }}</span>
</div>
<div class="d-flex">
<p style="text-wrap: nowrap">{{ $t('federation.hieroTopicId') }}&nbsp;</p>
<span class="d-block" style="overflow-x: auto">{{ hieroTopicId }}</span>
</div>
<BFormGroup>
{{ $t('federation.coordinates') }}
<span v-if="isValidLocation">
@ -57,6 +64,11 @@
:label="$t('federation.gmsApiKey')"
id-name="home-community-api-key"
/>
<editable-groupable-label
v-model="hieroTopicId"
:label="$t('federation.hieroTopicId')"
id-name="home-community-hiero-topic-id"
/>
<coordinates v-model="location" />
</template>
</editable-group>
@ -111,9 +123,11 @@ const { toastSuccess, toastError } = useAppToast()
const details = ref(false)
const gmsApiKey = ref(item.value.gmsApiKey)
const hieroTopicId = ref(item.value.hieroTopicId)
const location = ref(item.value.location)
const originalGmsApiKey = ref(item.value.gmsApiKey)
const originalLocation = ref(item.value.location)
const originalHieroTopicId = ref(item.value.hieroTopicId)
const { mutate: updateHomeCommunityMutation } = useMutation(updateHomeCommunity)
@ -164,6 +178,7 @@ const createdAt = computed(() => {
const isLocationChanged = computed(() => originalLocation.value !== location.value)
const isGMSApiKeyChanged = computed(() => originalGmsApiKey.value !== gmsApiKey.value)
const isHieroTopicIdChanged = computed(() => originalHieroTopicId.value !== hieroTopicId.value)
const isValidLocation = computed(
() => location.value && location.value.latitude && location.value.longitude,
)
@ -178,6 +193,7 @@ const handleUpdateHomeCommunity = async () => {
uuid: item.value.uuid,
gmsApiKey: gmsApiKey.value,
location: location.value,
hieroTopicId: hieroTopicId.value,
})
if (isLocationChanged.value && isGMSApiKeyChanged.value) {
@ -187,8 +203,12 @@ const handleUpdateHomeCommunity = async () => {
} else if (isLocationChanged.value) {
toastSuccess(t('federation.toast_gmsLocationUpdated'))
}
if (isHieroTopicIdChanged.value) {
toastSuccess(t('federation.toast_hieroTopicIdUpdated'))
}
originalLocation.value = location.value
originalGmsApiKey.value = gmsApiKey.value
originalHieroTopicId.value = hieroTopicId.value
} catch (error) {
toastError(error.message)
}
@ -197,5 +217,6 @@ const handleUpdateHomeCommunity = async () => {
const resetHomeCommunityEditable = () => {
location.value = originalLocation.value
gmsApiKey.value = originalGmsApiKey.value
hieroTopicId.value = originalHieroTopicId.value
}
</script>

View File

@ -15,6 +15,7 @@ export const allCommunities = gql`
creationDate
createdAt
updatedAt
hieroTopicId
federatedCommunities {
id
apiVersion

View File

@ -1,8 +1,13 @@
import gql from 'graphql-tag'
export const updateHomeCommunity = gql`
mutation ($uuid: String!, $gmsApiKey: String, $location: Location) {
updateHomeCommunity(uuid: $uuid, gmsApiKey: $gmsApiKey, location: $location) {
mutation ($uuid: String!, $gmsApiKey: String, $location: Location, $hieroTopicId: String) {
updateHomeCommunity(
uuid: $uuid
gmsApiKey: $gmsApiKey
location: $location
hieroTopicId: $hieroTopicId
) {
id
}
}

View File

@ -96,9 +96,11 @@
"coordinates": "Koordinaten:",
"createdAt": "Erstellt am",
"gmsApiKey": "GMS API Key:",
"hieroTopicId": "Hiero Topic ID:",
"toast_gmsApiKeyAndLocationUpdated": "Der GMS Api Key und die Location wurden erfolgreich aktualisiert!",
"toast_gmsApiKeyUpdated": "Der GMS Api Key wurde erfolgreich aktualisiert!",
"toast_gmsLocationUpdated": "Die GMS Location wurde erfolgreich aktualisiert!",
"toast_hieroTopicIdUpdated": "Die Hiero Topic ID wurde erfolgreich aktualisiert!",
"gradidoInstances": "Gradido Instanzen",
"lastAnnouncedAt": "letzte Bekanntgabe",
"lastErrorAt": "Letzer Fehler am",

View File

@ -96,9 +96,11 @@
"coordinates": "Coordinates:",
"createdAt": "Created At ",
"gmsApiKey": "GMS API Key:",
"hieroTopicId": "Hiero Topic ID:",
"toast_gmsApiKeyAndLocationUpdated": "The GMS Api Key and the location have been successfully updated!",
"toast_gmsApiKeyUpdated": "The GMS Api Key has been successfully updated!",
"toast_gmsLocationUpdated": "The GMS location has been successfully updated!",
"toast_hieroTopicIdUpdated": "The Hiero Topic ID has been successfully updated!",
"gradidoInstances": "Gradido Instances",
"lastAnnouncedAt": "Last Announced",
"lastErrorAt": "last error at",

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "backend",
"version": "2.6.0",
"version": "2.6.1",
"private": false,
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"repository": "https://github.com/gradido/gradido/backend",

View File

@ -1 +1,3 @@
export const LOG4JS_BASE_CATEGORY_NAME = 'backend'
export const FRONTEND_LOGIN_ROUTE = 'login'
export const GRADIDO_REALM = 'gradido'

View File

@ -6,14 +6,11 @@ import { ensureUrlEndsWithSlash } from '@/util/utilities'
import { getLogger } from 'log4js'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { SendCoinsArgsLoggingView } from './logging/SendCoinsArgsLogging.view'
import { SendCoinsResultLoggingView } from './logging/SendCoinsResultLogging.view'
import { SendCoinsArgs } from './model/SendCoinsArgs'
import { SendCoinsResult } from './model/SendCoinsResult'
import { revertSendCoins as revertSendCoinsQuery } from './query/revertSendCoins'
import { revertSettledSendCoins as revertSettledSendCoinsQuery } from './query/revertSettledSendCoins'
import { settleSendCoins as settleSendCoinsQuery } from './query/settleSendCoins'
import { voteForSendCoins as voteForSendCoinsQuery } from './query/voteForSendCoins'
import { EncryptedTransferArgs } from 'core'
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.client.1_0.SendCoinsClient`)
@ -34,33 +31,26 @@ export class SendCoinsClient {
})
}
async voteForSendCoins(args: SendCoinsArgs): Promise<SendCoinsResult> {
async voteForSendCoins(args: EncryptedTransferArgs): Promise<string | null> {
logger.debug('voteForSendCoins against endpoint=', this.endpoint)
try {
logger.debug(`voteForSendCoins with args=`, new SendCoinsArgsLoggingView(args))
const { data } = await this.client.rawRequest<{ voteForSendCoins: SendCoinsResult }>(
voteForSendCoinsQuery,
{ args },
)
const result = data.voteForSendCoins
if (!data?.voteForSendCoins?.vote) {
logger.debug('voteForSendCoins failed with: ', new SendCoinsResultLoggingView(result))
return new SendCoinsResult()
const { data } = await this.client.rawRequest<{ voteForSendCoins: string }>(voteForSendCoinsQuery, { args })
const responseJwt = data?.voteForSendCoins
if (responseJwt) {
logger.debug('received response jwt', responseJwt)
return responseJwt
}
logger.debug(
'voteForSendCoins successful with result=',
new SendCoinsResultLoggingView(result),
)
return result
} catch (err) {
throw new LogError(`voteForSendCoins failed for endpoint=${this.endpoint}:`, err)
const errmsg = `voteForSendCoins failed for endpoint=${this.endpoint}, err=${err}`
logger.error(errmsg)
throw new Error(errmsg)
}
return null
}
async revertSendCoins(args: SendCoinsArgs): Promise<boolean> {
async revertSendCoins(args: EncryptedTransferArgs): Promise<boolean> {
logger.debug('revertSendCoins against endpoint=', this.endpoint)
try {
logger.debug(`revertSendCoins with args=`, new SendCoinsArgsLoggingView(args))
const { data } = await this.client.rawRequest<{ revertSendCoins: boolean }>(
revertSendCoinsQuery,
{ args },
@ -78,10 +68,9 @@ export class SendCoinsClient {
}
}
async settleSendCoins(args: SendCoinsArgs): Promise<boolean> {
async settleSendCoins(args: EncryptedTransferArgs): Promise<boolean> {
logger.debug(`settleSendCoins against endpoint='${this.endpoint}'...`)
try {
logger.debug(`settleSendCoins with args=`, new SendCoinsArgsLoggingView(args))
const { data } = await this.client.rawRequest<{ settleSendCoins: boolean }>(
settleSendCoinsQuery,
{ args },
@ -98,10 +87,9 @@ export class SendCoinsClient {
}
}
async revertSettledSendCoins(args: SendCoinsArgs): Promise<boolean> {
async revertSettledSendCoins(args: EncryptedTransferArgs): Promise<boolean> {
logger.debug(`revertSettledSendCoins against endpoint='${this.endpoint}'...`)
try {
logger.debug(`revertSettledSendCoins with args=`, new SendCoinsArgsLoggingView(args))
const { data } = await this.client.rawRequest<{ revertSettledSendCoins: boolean }>(
revertSettledSendCoinsQuery,
{ args },

View File

@ -1,7 +1,7 @@
import { gql } from 'graphql-request'
export const revertSendCoins = gql`
mutation ($args: SendCoinsArgs!) {
mutation ($args: EncryptedTransferArgs!) {
revertSendCoins(data: $args)
}
`

View File

@ -1,7 +1,7 @@
import { gql } from 'graphql-request'
export const revertSettledSendCoins = gql`
mutation ($args: SendCoinsArgs!) {
mutation ($args: EncryptedTransferArgs!) {
revertSettledSendCoins(data: $args)
}
`

View File

@ -1,7 +1,7 @@
import { gql } from 'graphql-request'
export const settleSendCoins = gql`
mutation ($args: SendCoinsArgs!) {
mutation ($args: EncryptedTransferArgs!) {
settleSendCoins(data: $args)
}
`

View File

@ -1,8 +1,12 @@
import { gql } from 'graphql-request'
export const voteForSendCoins = gql`
mutation ($args: SendCoinsArgs!) {
voteForSendCoins(data: $args) {
mutation ($args: EncryptedTransferArgs!) {
voteForSendCoins(data: $args)
}
`
/*
{
vote
recipGradidoID
recipFirstName
@ -10,4 +14,4 @@ export const voteForSendCoins = gql`
recipAlias
}
}
`
*/

View File

@ -21,7 +21,7 @@ export class EditCommunityInput {
location?: Location | null
@Field(() => String, { nullable: true })
@IsString()
@isValidHieroId()
topicId?: string | null
hieroTopicId?: string | null
}

View File

@ -39,6 +39,7 @@ export class AdminCommunityView {
this.uuid = dbCom.communityUuid
this.authenticatedAt = dbCom.authenticatedAt
this.gmsApiKey = dbCom.gmsApiKey
this.hieroTopicId = dbCom.hieroTopicId
if (dbCom.location) {
this.location = Point2Location(dbCom.location as Point)
}
@ -71,6 +72,9 @@ export class AdminCommunityView {
@Field(() => Location, { nullable: true })
location: Location | null
@Field(() => String, { nullable: true })
hieroTopicId: string | null
@Field(() => Date, { nullable: true })
creationDate: Date | null

View File

@ -13,6 +13,7 @@ export class Community {
this.uuid = dbCom.communityUuid
this.authenticatedAt = dbCom.authenticatedAt
this.gmsApiKey = dbCom.gmsApiKey
this.hieroTopicId = dbCom.hieroTopicId
}
@Field(() => Int)
@ -41,4 +42,7 @@ export class Community {
@Field(() => String, { nullable: true })
gmsApiKey: string | null
@Field(() => String, { nullable: true })
hieroTopicId: string | null
}

View File

@ -38,6 +38,7 @@ export class CommunityResolver {
@Authorized([RIGHTS.COMMUNITIES])
@Query(() => [AdminCommunityView])
async allCommunities(@Args() paginated: Paginated): Promise<AdminCommunityView[]> {
// communityUUID could be oneTimePassCode (uint32 number)
return (await getAllCommunities(paginated)).map((dbCom) => new AdminCommunityView(dbCom))
}
@ -58,6 +59,7 @@ export class CommunityResolver {
async communityByIdentifier(
@Arg('communityIdentifier') communityIdentifier: string,
): Promise<Community> {
// communityUUID could be oneTimePassCode (uint32 number)
const community = await getCommunityByIdentifier(communityIdentifier)
if (!community) {
throw new LogError('community not found', communityIdentifier)
@ -78,7 +80,7 @@ export class CommunityResolver {
@Authorized([RIGHTS.COMMUNITY_UPDATE])
@Mutation(() => Community)
async updateHomeCommunity(
@Args() { uuid, gmsApiKey, location }: EditCommunityInput,
@Args() { uuid, gmsApiKey, location, hieroTopicId }: EditCommunityInput,
): Promise<Community> {
const homeCom = await getCommunityByUuid(uuid)
if (!homeCom) {
@ -87,11 +89,16 @@ export class CommunityResolver {
if (homeCom.foreign) {
throw new LogError('Error: Only the HomeCommunity could be modified!')
}
if (homeCom.gmsApiKey !== gmsApiKey || homeCom.location !== location) {
if (
homeCom.gmsApiKey !== gmsApiKey ||
homeCom.location !== location ||
homeCom.hieroTopicId !== hieroTopicId
) {
homeCom.gmsApiKey = gmsApiKey ?? null
if (location) {
homeCom.location = Location2Point(location)
}
homeCom.hieroTopicId = hieroTopicId ?? null
await DbCommunity.save(homeCom)
}
return new Community(homeCom)

View File

@ -423,13 +423,13 @@ describe('Contribution Links', () => {
])
})
it('returns an error if memo is longer than 255 characters', async () => {
it('returns an error if memo is longer than 512 characters', async () => {
jest.clearAllMocks()
const { errors: errorObjects } = await mutate({
mutation: createContributionLink,
variables: {
...variables,
memo: '1234567890123456789212345678931234567894123456789512345678961234567897123456789812345678991234567890123456789012345678921234567893123456789412345678951234567896123456789712345678981234567899123456789012345678901234567892123456789312345678941234567895123456',
memo: '123456789012345678921234567893123456789412345678951234567896123456789712345678981234567899123456789012345678901234567892123456789312345678941234567895123456789612345678971234567898123456789912345678901234567890123456789212345678931234567894123456789512345612345678901234567892123456789312345678941234567895123456789612345678971234567898123456789912345678901234567890123456789212345678931234567894123456789512345678961234567897123456789812345678991234567890123456789012345678921234567893123456789412345678951234567',
},
})
expect(errorObjects).toMatchObject([
@ -441,7 +441,7 @@ describe('Contribution Links', () => {
{
property: 'memo',
constraints: {
maxLength: 'memo must be shorter than or equal to 255 characters',
maxLength: 'memo must be shorter than or equal to 512 characters',
},
},
],

View File

@ -229,14 +229,14 @@ describe('ContributionResolver', () => {
])
})
it('throws error when memo length greater than 255 chars', async () => {
it('throws error when memo length greater than 512 chars', async () => {
jest.clearAllMocks()
const date = new Date()
const { errors: errorObjects } = await mutate({
mutation: createContribution,
variables: {
amount: 100.0,
memo: 'Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test',
memo: 'Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test',
contributionDate: date.toString(),
},
})
@ -249,7 +249,7 @@ describe('ContributionResolver', () => {
{
property: 'memo',
constraints: {
maxLength: 'memo must be shorter than or equal to 255 characters',
maxLength: 'memo must be shorter than or equal to 512 characters',
},
},
],
@ -398,7 +398,7 @@ describe('ContributionResolver', () => {
})
})
describe('Memo length greater than 255 chars', () => {
describe('Memo length greater than 512 chars', () => {
it('throws error', async () => {
jest.clearAllMocks()
const date = new Date()
@ -407,7 +407,7 @@ describe('ContributionResolver', () => {
variables: {
contributionId: pendingContribution.data.createContribution.id,
amount: 100.0,
memo: 'Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test',
memo: 'Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test Test',
contributionDate: date.toString(),
},
})
@ -420,7 +420,7 @@ describe('ContributionResolver', () => {
{
property: 'memo',
constraints: {
maxLength: 'memo must be shorter than or equal to 255 characters',
maxLength: 'memo must be shorter than or equal to 512 characters',
},
},
],

View File

@ -187,7 +187,7 @@ describe('TransactionLinkResolver', () => {
variables: {
identifier: 'peter@lustig.de',
amount: 100,
memo: 'test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test t',
memo: 'test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test',
},
})
expect(errorObjects).toMatchObject([
@ -199,7 +199,7 @@ describe('TransactionLinkResolver', () => {
{
property: 'memo',
constraints: {
maxLength: 'memo must be shorter than or equal to 255 characters',
maxLength: 'memo must be shorter than or equal to 512 characters',
},
},
],

View File

@ -269,7 +269,7 @@ describe('send coins', () => {
recipientCommunityIdentifier: homeCom.communityUuid,
recipientIdentifier: 'peter@lustig.de',
amount: 100,
memo: 'test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test t',
memo: 'test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test',
},
})
expect(errorObjects).toMatchObject([
@ -281,7 +281,7 @@ describe('send coins', () => {
{
property: 'memo',
constraints: {
maxLength: 'memo must be shorter than or equal to 255 characters',
maxLength: 'memo must be shorter than or equal to 512 characters',
},
},
],
@ -479,7 +479,6 @@ describe('send coins', () => {
})
})
})
describe('send coins via gradido ID', () => {
it('sends the coins', async () => {
await expect(
@ -591,8 +590,8 @@ describe('send coins', () => {
})
})
})
describe('X-Com send coins via gradido ID', () => {
/*
describe.skip('X-Com send coins via gradido ID', () => {
beforeAll(async () => {
CONFIG.FEDERATION_XCOM_SENDCOINS_ENABLED = true
fedForeignCom = DbFederatedCommunity.create()
@ -653,7 +652,7 @@ describe('send coins', () => {
})
})
})
*/
describe('more transactions to test semaphore', () => {
it('sends the coins four times in a row', async () => {
await expect(

View File

@ -15,7 +15,7 @@ import { In, IsNull } from 'typeorm'
import { Paginated } from '@arg/Paginated'
import { TransactionSendArgs } from '@arg/TransactionSendArgs'
import { Order } from '@enum/Order'
import { PendingTransactionState } from 'shared'
import { PendingTransactionState, SendCoinsResponseJwtPayloadType } from 'shared'
import { TransactionTypeId } from '@enum/TransactionTypeId'
import { Transaction } from '@model/Transaction'
import { TransactionList } from '@model/TransactionList'
@ -484,7 +484,7 @@ export class TransactionResolver {
if (recipCom !== null && recipCom.authenticatedAt === null) {
throw new LogError('recipient community is connected, but still not authenticated yet!')
}
let pendingResult: SendCoinsResult
let pendingResult: SendCoinsResponseJwtPayloadType | null = null
let committingResult: SendCoinsResult
const creationDate = new Date()
@ -499,7 +499,7 @@ export class TransactionResolver {
recipientIdentifier,
)
logger.debug('processXComPendingSendCoins result: ', pendingResult)
if (pendingResult.vote && pendingResult.recipGradidoID) {
if (pendingResult && pendingResult.vote && pendingResult.recipGradidoID) {
logger.debug('vor processXComCommittingSendCoins... ')
committingResult = await processXComCommittingSendCoins(
recipCom,

View File

@ -8,7 +8,7 @@ export const FULL_CREATION_AVAILABLE = [
]
export const CONTRIBUTIONLINK_NAME_MAX_CHARS = 100
export const CONTRIBUTIONLINK_NAME_MIN_CHARS = 5
export const MEMO_MAX_CHARS = 255
export const MEMO_MAX_CHARS = 512
export const MEMO_MIN_CHARS = 5
export const DEFAULT_PAGINATION_PAGE_SIZE = 25
export const FRONTEND_CONTRIBUTIONS_ITEM_ANCHOR_PREFIX = 'contributionListItem-'

View File

@ -16,7 +16,7 @@ import { SendCoinsClient as V1_0_SendCoinsClient } from '@/federation/client/1_0
import { SendCoinsArgs } from '@/federation/client/1_0/model/SendCoinsArgs'
import { SendCoinsResult } from '@/federation/client/1_0/model/SendCoinsResult'
import { SendCoinsClientFactory } from '@/federation/client/SendCoinsClientFactory'
import { PendingTransactionState } from 'shared'
import { encryptAndSign, PendingTransactionState, SendCoinsJwtPayloadType, SendCoinsResponseJwtPayloadType, verifyAndDecrypt } from 'shared'
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { LogError } from '@/server/LogError'
@ -27,8 +27,10 @@ import { getLogger } from 'log4js'
import { settlePendingSenderTransaction } from './settlePendingSenderTransaction'
import { SendCoinsArgsLoggingView } from '@/federation/client/1_0/logging/SendCoinsArgsLogging.view'
import { SendCoinsResultLoggingView } from '@/federation/client/1_0/logging/SendCoinsResultLogging.view'
import { EncryptedTransferArgs } from 'core'
import { randombytes_random } from 'sodium-native'
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.processXComSendCoins`)
const createLogger = (method: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.processXComSendCoins.${method}`)
export async function processXComPendingSendCoins(
receiverCom: DbCommunity,
@ -38,12 +40,13 @@ export async function processXComPendingSendCoins(
memo: string,
sender: dbUser,
recipientIdentifier: string,
): Promise<SendCoinsResult> {
let voteResult: SendCoinsResult
): Promise<SendCoinsResponseJwtPayloadType | null> {
let voteResult: SendCoinsResponseJwtPayloadType
const methodLogger = createLogger(`processXComPendingSendCoins`)
try {
// even if debug is not enabled, attributes are processed so we skip the entire call for performance reasons
if(logger.isDebugEnabled()) {
logger.debug(
if(methodLogger.isDebugEnabled()) {
methodLogger.debug(
'XCom: processXComPendingSendCoins...', {
receiverCom: new CommunityLoggingView(receiverCom),
senderCom: new CommunityLoggingView(senderCom),
@ -55,17 +58,21 @@ export async function processXComPendingSendCoins(
)
}
if (await countOpenPendingTransactions([sender.gradidoID, recipientIdentifier]) > 0) {
throw new LogError(
`There exist still ongoing 'Pending-Transactions' for the involved users on sender-side!`,
)
const errmsg = `There exist still ongoing 'Pending-Transactions' for the involved users on sender-side!`
methodLogger.error(errmsg)
throw new LogError(errmsg)
}
const handshakeID = randombytes_random().toString()
methodLogger.addContext('handshakeID', handshakeID)
// first calculate the sender balance and check if the transaction is allowed
const senderBalance = await calculateSenderBalance(sender.id, amount.mul(-1), creationDate)
if (!senderBalance) {
throw new LogError('User has not enough GDD or amount is < 0', senderBalance)
const errmsg = `User has not enough GDD or amount is < 0`
methodLogger.error(errmsg)
throw new LogError(errmsg)
}
if(logger.isDebugEnabled()) {
logger.debug(`calculated senderBalance = ${JSON.stringify(senderBalance, null, 2)}`)
if(methodLogger.isDebugEnabled()) {
methodLogger.debug(`calculated senderBalance = ${JSON.stringify(senderBalance, null, 2)}`)
}
const receiverFCom = await DbFederatedCommunity.findOneOrFail({
@ -77,89 +84,115 @@ export async function processXComPendingSendCoins(
const client = SendCoinsClientFactory.getInstance(receiverFCom)
if (client instanceof V1_0_SendCoinsClient) {
const args = new SendCoinsArgs()
if (receiverCom.communityUuid) {
args.recipientCommunityUuid = receiverCom.communityUuid
const payload = new SendCoinsJwtPayloadType(handshakeID,
receiverCom.communityUuid!,
recipientIdentifier,
creationDate.toISOString(),
amount,
memo,
senderCom.communityUuid!,
sender.gradidoID,
fullName(sender.firstName, sender.lastName),
sender.alias
)
if(methodLogger.isDebugEnabled()) {
methodLogger.debug(`ready for voteForSendCoins with payload=${payload}`)
}
args.recipientUserIdentifier = recipientIdentifier
args.creationDate = creationDate.toISOString()
args.amount = amount
args.memo = memo
if (senderCom.communityUuid) {
args.senderCommunityUuid = senderCom.communityUuid
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, receiverCom.publicJwtKey!)
if(methodLogger.isDebugEnabled()) {
methodLogger.debug('jws', jws)
}
args.senderUserUuid = sender.gradidoID
args.senderUserName = fullName(sender.firstName, sender.lastName)
args.senderAlias = sender.alias
if(logger.isDebugEnabled()) {
logger.debug(`ready for voteForSendCoins with args=${new SendCoinsArgsLoggingView(args)}`)
// prepare the args for the client invocation
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = handshakeID
if(methodLogger.isDebugEnabled()) {
methodLogger.debug('before client.voteForSendCoins() args:', args)
}
voteResult = await client.voteForSendCoins(args)
if(logger.isDebugEnabled()) {
logger.debug(`returned from voteForSendCoins: ${new SendCoinsResultLoggingView(voteResult)}`)
}
if (voteResult.vote) {
logger.debug('prepare pendingTransaction for sender...')
// writing the pending transaction on receiver-side was successfull, so now write the sender side
try {
const pendingTx = DbPendingTransaction.create()
pendingTx.amount = amount.mul(-1)
pendingTx.balance = senderBalance.balance
pendingTx.balanceDate = creationDate
pendingTx.decay = senderBalance ? senderBalance.decay.decay : new Decimal(0)
pendingTx.decayStart = senderBalance ? senderBalance.decay.start : null
if (receiverCom.communityUuid) {
pendingTx.linkedUserCommunityUuid = receiverCom.communityUuid
}
if (voteResult.recipGradidoID) {
pendingTx.linkedUserGradidoID = voteResult.recipGradidoID
}
if (voteResult.recipFirstName && voteResult.recipLastName) {
pendingTx.linkedUserName = fullName(voteResult.recipFirstName, voteResult.recipLastName)
}
pendingTx.memo = memo
pendingTx.previous = senderBalance ? senderBalance.lastTransactionId : null
pendingTx.state = PendingTransactionState.NEW
pendingTx.typeId = TransactionTypeId.SEND
if (senderCom.communityUuid) {
pendingTx.userCommunityUuid = senderCom.communityUuid
}
pendingTx.userId = sender.id
pendingTx.userGradidoID = sender.gradidoID
pendingTx.userName = fullName(sender.firstName, sender.lastName)
if(logger.isDebugEnabled()) {
logger.debug(`initialized sender pendingTX=${new PendingTransactionLoggingView(pendingTx)}`)
}
await DbPendingTransaction.insert(pendingTx)
logger.debug('sender pendingTx successfully inserted...')
} catch (err) {
logger.error(`Error in writing sender pending transaction: ${JSON.stringify(err, null, 2)}`)
// revert the existing pending transaction on receiver side
let revertCount = 0
logger.debug('first try to revertSendCoins of receiver')
do {
if (await client.revertSendCoins(args)) {
logger.debug(`revertSendCoins()-1_0... successfull after revertCount=${revertCount}`)
// treat revertingSendCoins as an error of the whole sendCoins-process
throw new LogError('Error in writing sender pending transaction: ', err)
}
} while (CONFIG.FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS > revertCount++)
throw new LogError(
`Error in reverting receiver pending transaction even after revertCount=${revertCount}`,
err,
)
}
logger.debug('voteForSendCoins()-1_0... successfull')
} else {
logger.error(`break with error on writing pendingTransaction for recipient... ${new SendCoinsResultLoggingView(voteResult)}`)
const responseJwt = await client.voteForSendCoins(args)
if(methodLogger.isDebugEnabled()) {
methodLogger.debug(`response of voteForSendCoins():`, responseJwt)
}
if (responseJwt !== null) {
voteResult = await verifyAndDecrypt(handshakeID, responseJwt, senderCom.privateJwtKey!, receiverCom.publicJwtKey!) as SendCoinsResponseJwtPayloadType
if(methodLogger.isDebugEnabled()) {
methodLogger.debug(`received payload from voteForSendCoins():`, voteResult)
}
if (voteResult && voteResult.tokentype !== SendCoinsResponseJwtPayloadType.SEND_COINS_RESPONSE_TYPE) {
const errmsg = `Invalid tokentype in voteForSendCoins-response of community with publicKey` + receiverCom.publicKey
methodLogger.error(errmsg)
throw new Error('Error in X-Com-TX protocol...')
}
if (voteResult && voteResult.vote) {
methodLogger.debug('prepare pendingTransaction for sender...')
// writing the pending transaction on receiver-side was successfull, so now write the sender side
try {
const pendingTx = DbPendingTransaction.create()
pendingTx.amount = amount.mul(-1)
pendingTx.balance = senderBalance.balance
pendingTx.balanceDate = creationDate
pendingTx.decay = senderBalance ? senderBalance.decay.decay : new Decimal(0)
pendingTx.decayStart = senderBalance ? senderBalance.decay.start : null
if (receiverCom.communityUuid) {
pendingTx.linkedUserCommunityUuid = receiverCom.communityUuid
}
if (voteResult.recipGradidoID) {
pendingTx.linkedUserGradidoID = voteResult.recipGradidoID
}
if (voteResult.recipFirstName && voteResult.recipLastName) {
pendingTx.linkedUserName = fullName(voteResult.recipFirstName, voteResult.recipLastName)
}
pendingTx.memo = memo
pendingTx.previous = senderBalance ? senderBalance.lastTransactionId : null
pendingTx.state = PendingTransactionState.NEW
pendingTx.typeId = TransactionTypeId.SEND
if (senderCom.communityUuid) {
pendingTx.userCommunityUuid = senderCom.communityUuid
}
pendingTx.userId = sender.id
pendingTx.userGradidoID = sender.gradidoID
pendingTx.userName = fullName(sender.firstName, sender.lastName)
if(methodLogger.isDebugEnabled()) {
methodLogger.debug(`initialized sender pendingTX=${new PendingTransactionLoggingView(pendingTx)}`)
}
await DbPendingTransaction.insert(pendingTx)
methodLogger.debug('sender pendingTx successfully inserted...')
} catch (err) {
methodLogger.error(`Error in writing sender pending transaction: ${JSON.stringify(err, null, 2)}`)
// revert the existing pending transaction on receiver side
let revertCount = 0
methodLogger.debug('first try to revertSendCoins of receiver')
do {
if (await client.revertSendCoins(args)) {
methodLogger.debug(`revertSendCoins()-1_0... successfull after revertCount=${revertCount}`)
// treat revertingSendCoins as an error of the whole sendCoins-process
const errmsg = `Error in writing sender pending transaction: ${JSON.stringify(err, null, 2)}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
} while (CONFIG.FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS > revertCount++)
const errmsg = `Error in reverting receiver pending transaction even after revertCount=${revertCount}` + JSON.stringify(err, null, 2)
methodLogger.error(errmsg)
throw new Error(errmsg)
}
methodLogger.debug('voteForSendCoins()-1_0... successfull')
return voteResult
} else {
methodLogger.error(`break with error on writing pendingTransaction for recipient... ${voteResult}`)
}
} else {
methodLogger.error(`break with no response from voteForSendCoins()-1_0...`)
}
return voteResult
}
} catch (err: any) {
throw new LogError(`Error: ${err.message}`, err)
} catch (err: any) {
const errmsg = `Error: ${err.message}` + JSON.stringify(err, null, 2)
methodLogger.error(errmsg)
throw new Error(errmsg)
}
return new SendCoinsResult()
return null
}
export async function processXComCommittingSendCoins(
@ -171,10 +204,13 @@ export async function processXComCommittingSendCoins(
sender: dbUser,
recipient: SendCoinsResult,
): Promise<SendCoinsResult> {
const methodLogger = createLogger(`processXComCommittingSendCoins`)
const handshakeID = randombytes_random().toString()
methodLogger.addContext('handshakeID', handshakeID)
const sendCoinsResult = new SendCoinsResult()
try {
if(logger.isDebugEnabled()) {
logger.debug(
if(methodLogger.isDebugEnabled()) {
methodLogger.debug(
'XCom: processXComCommittingSendCoins...', {
receiverCom: new CommunityLoggingView(receiverCom),
senderCom: new CommunityLoggingView(senderCom),
@ -200,40 +236,49 @@ export async function processXComCommittingSendCoins(
memo,
})
if (pendingTx) {
if(logger.isDebugEnabled()) {
logger.debug(`find pending Tx for settlement: ${new PendingTransactionLoggingView(pendingTx)}`)
if(methodLogger.isDebugEnabled()) {
methodLogger.debug(`find pending Tx for settlement: ${new PendingTransactionLoggingView(pendingTx)}`)
}
const receiverFCom = await DbFederatedCommunity.findOneOrFail({
where: {
publicKey: Buffer.from(receiverCom.publicKey),
apiVersion: CONFIG.FEDERATION_BACKEND_SEND_ON_API,
},
})
const client = SendCoinsClientFactory.getInstance(receiverFCom)
if (client instanceof V1_0_SendCoinsClient) {
const args = new SendCoinsArgs()
args.recipientCommunityUuid = pendingTx.linkedUserCommunityUuid
const payload = new SendCoinsJwtPayloadType(
handshakeID,
pendingTx.linkedUserCommunityUuid
? pendingTx.linkedUserCommunityUuid
: CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID,
pendingTx.linkedUserGradidoID!,
pendingTx.balanceDate.toISOString(),
pendingTx.amount.mul(-1),
pendingTx.memo,
pendingTx.userCommunityUuid,
pendingTx.userGradidoID!,
pendingTx.userName!,
sender.alias,
)
payload.recipientCommunityUuid = pendingTx.linkedUserCommunityUuid
? pendingTx.linkedUserCommunityUuid
: CONFIG.FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID
if (pendingTx.linkedUserGradidoID) {
args.recipientUserIdentifier = pendingTx.linkedUserGradidoID
payload.recipientUserIdentifier = pendingTx.linkedUserGradidoID
}
args.creationDate = pendingTx.balanceDate.toISOString()
args.amount = pendingTx.amount.mul(-1)
args.memo = pendingTx.memo
args.senderCommunityUuid = pendingTx.userCommunityUuid
args.senderUserUuid = pendingTx.userGradidoID
if (pendingTx.userName) {
args.senderUserName = pendingTx.userName
}
args.senderAlias = sender.alias
if(logger.isDebugEnabled()) {
logger.debug(`ready for settleSendCoins with args=${new SendCoinsArgsLoggingView(args)}`)
if(methodLogger.isDebugEnabled()) {
methodLogger.debug(`ready for settleSendCoins with payload=${ JSON.stringify(payload)}`)
}
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, receiverCom.publicJwtKey!)
// prepare the args for the client invocation
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = handshakeID
const acknowledge = await client.settleSendCoins(args)
logger.debug(`returnd from settleSendCoins: ${acknowledge}`)
methodLogger.debug(`return from settleSendCoins: ${acknowledge}`)
if (acknowledge) {
// settle the pending transaction on receiver-side was successfull, so now settle the sender side
try {
@ -257,13 +302,13 @@ export async function processXComCommittingSendCoins(
sendCoinsResult.recipAlias = recipient.recipAlias
}
} catch (err) {
logger.error(`Error in writing sender pending transaction: ${JSON.stringify(err, null, 2)}`)
methodLogger.error(`Error in writing sender pending transaction: ${JSON.stringify(err, null, 2)}`)
// revert the existing pending transaction on receiver side
let revertCount = 0
logger.debug('first try to revertSetteledSendCoins of receiver')
methodLogger.debug('first try to revertSetteledSendCoins of receiver')
do {
if (await client.revertSettledSendCoins(args)) {
logger.debug(
methodLogger.debug(
`revertSettledSendCoins()-1_0... successfull after revertCount=${revertCount}`,
)
// treat revertingSettledSendCoins as an error of the whole sendCoins-process
@ -279,7 +324,7 @@ export async function processXComCommittingSendCoins(
}
}
} catch (err) {
logger.error(`Error: ${JSON.stringify(err, null, 2)}`)
methodLogger.error(`Error: ${JSON.stringify(err, null, 2)}`)
sendCoinsResult.vote = false
}
return sendCoinsResult

View File

@ -1,8 +1,11 @@
import {
AppDatabase,
CommunityLoggingView,
Community as DbCommunity,
PendingTransaction as DbPendingTransaction,
User as DbUser,
PendingTransactionLoggingView,
UserLoggingView,
Transaction as dbTransaction,
} from 'database'
import { Decimal } from 'decimal.js-light'
@ -34,7 +37,7 @@ export async function settlePendingSenderTransaction(
logger.debug(`start Transaction for write-access...`)
try {
logger.info('settlePendingSenderTransaction:', homeCom, senderUser, pendingTx)
logger.info('settlePendingSenderTransaction:', new CommunityLoggingView(homeCom), new UserLoggingView(senderUser), new PendingTransactionLoggingView(pendingTx))
// ensure that no other pendingTx with the same sender or recipient exists
const openSenderPendingTx = await DbPendingTransaction.count({
@ -88,7 +91,7 @@ export async function settlePendingSenderTransaction(
transactionSend.previous = pendingTx.previous
transactionSend.linkedTransactionId = pendingTx.linkedTransactionId
await queryRunner.manager.insert(dbTransaction, transactionSend)
logger.debug(`send Transaction inserted: ${dbTransaction}`)
logger.debug(`send Transaction inserted: ${transactionSend}`)
// and mark the pendingTx in the pending_transactions table as settled
pendingTx.state = PendingTransactionState.SETTLED

View File

@ -0,0 +1,63 @@
import { CONFIG } from '@/config'
import { FRONTEND_LOGIN_ROUTE, GRADIDO_REALM, LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { getHomeCommunity } from 'database'
import { importSPKI, exportJWK } from 'jose'
import { createHash } from 'crypto'
import { getLogger } from 'log4js'
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.openIDConnect`)
const defaultErrorForCaller = 'Internal Server Error'
export const openidConfiguration = async (req: any, res: any): Promise<void> => {
res.setHeader('Content-Type', 'application/json')
res.status(200).json({
issuer: new URL(FRONTEND_LOGIN_ROUTE, CONFIG.COMMUNITY_URL).toString(),
jwks_uri: new URL(`/realms/${GRADIDO_REALM}/protocol/openid-connect/certs`, CONFIG.COMMUNITY_URL).toString(),
})
}
export const jwks = async (req: any, res: any): Promise<void> => {
const homeCommunity = await getHomeCommunity()
if (!homeCommunity) {
logger.error('HomeCommunity not found')
throw new Error(defaultErrorForCaller)
}
if (!homeCommunity.publicJwtKey) {
logger.error('HomeCommunity publicJwtKey not found')
throw new Error(defaultErrorForCaller)
}
try {
const rs256Key = await importSPKI(homeCommunity.publicJwtKey, 'RS256')
const rsaKey = await importSPKI(homeCommunity.publicJwtKey, 'RSA-OAEP-256')
const jwkRs256 = await exportJWK(rs256Key)
const jwkRsa = await exportJWK(rsaKey)
// Optional: calculate Key ID (z.B. SHA-256 Fingerprint)
const kid = createHash('sha256')
.update(homeCommunity.publicJwtKey)
.digest('base64url')
const jwks = {
keys: [
{
...jwkRs256,
alg: 'RS256',
use: 'sig',
kid,
},
{
...jwkRsa,
alg: 'RSA-OAEP-256',
use: 'sig',
kid,
},
],
}
res.setHeader('Cache-Control', 'public, max-age=3600, immutable')
res.setHeader('Content-Type', 'application/json')
res.status(200).json(jwks)
} catch (err) {
logger.error('Failed to convert publicJwtKey to JWK', err)
res.status(500).json({ error: 'Failed to generate JWKS' })
}
}

View File

@ -9,12 +9,13 @@ import helmet from 'helmet'
import { Logger, getLogger } from 'log4js'
import { DataSource } from 'typeorm'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { GRADIDO_REALM, LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { AppDatabase } from 'database'
import { context as serverContext } from './context'
import { cors } from './cors'
import { i18n } from './localization'
import { plugins } from './plugins'
import { jwks, openidConfiguration } from '@/openIDConnect'
// TODO implement
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
@ -84,6 +85,10 @@ export const createServer = async (
app.get('/hook/gms/' + CONFIG.GMS_WEBHOOK_SECRET, gmsWebhook)
// OpenID Connect
app.get(`/realms/${GRADIDO_REALM}/.well-known/openid-configuration`, openidConfiguration)
app.get(`/realms/${GRADIDO_REALM}/protocol/openid-connect/certs`, jwks)
// Apollo Server
const apollo = new ApolloServer({
schema: await schema(),

View File

@ -16,7 +16,7 @@
},
"admin": {
"name": "admin",
"version": "2.6.0",
"version": "2.6.1",
"dependencies": {
"@iconify/json": "^2.2.228",
"@popperjs/core": "^2.11.8",
@ -85,7 +85,7 @@
},
"backend": {
"name": "backend",
"version": "2.6.0",
"version": "2.6.1",
"dependencies": {
"cross-env": "^7.0.3",
"email-templates": "^10.0.1",
@ -162,7 +162,7 @@
},
"config-schema": {
"name": "config-schema",
"version": "2.6.0",
"version": "2.6.1",
"dependencies": {
"esbuild": "^0.25.2",
"joi": "^17.13.3",
@ -180,7 +180,7 @@
},
"core": {
"name": "core",
"version": "2.6.0",
"version": "2.6.1",
"dependencies": {
"database": "*",
"esbuild": "^0.25.2",
@ -198,7 +198,7 @@
},
"database": {
"name": "database",
"version": "2.6.0",
"version": "2.6.1",
"dependencies": {
"@types/uuid": "^8.3.4",
"cross-env": "^7.0.3",
@ -233,12 +233,12 @@
"ts-jest": "27.0.5",
"ts-node": "^10.9.2",
"typescript": "^4.9.5",
"vitest": "^3.2.4",
"vitest": "^2.0.5",
},
},
"dht-node": {
"name": "dht-node",
"version": "2.6.0",
"version": "2.6.1",
"dependencies": {
"cross-env": "^7.0.3",
"dht-rpc": "6.18.1",
@ -276,7 +276,7 @@
},
"federation": {
"name": "federation",
"version": "2.6.0",
"version": "2.6.1",
"dependencies": {
"cross-env": "^7.0.3",
"sodium-native": "^3.4.1",
@ -328,7 +328,7 @@
},
"frontend": {
"name": "frontend",
"version": "2.6.0",
"version": "2.6.1",
"dependencies": {
"@morev/vue-transitions": "^3.0.2",
"@types/leaflet": "^1.9.12",
@ -423,7 +423,7 @@
},
"shared": {
"name": "shared",
"version": "2.6.0",
"version": "2.6.1",
"dependencies": {
"decimal.js-light": "^2.5.1",
"esbuild": "^0.25.2",
@ -1011,8 +1011,6 @@
"@types/body-parser": ["@types/body-parser@1.19.5", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg=="],
"@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
"@types/content-disposition": ["@types/content-disposition@0.5.8", "", {}, "sha512-QVSSvno3dE0MgO76pJhmv4Qyi/j0Yk9pBp0Y7TJ2Tlj+KCgJWY6qX7nnxCOLkZ3VYRSIk1WTxCvwUSdx6CCLdg=="],
@ -1021,8 +1019,6 @@
"@types/cors": ["@types/cors@2.8.10", "", {}, "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
"@types/dotenv": ["@types/dotenv@8.2.3", "", { "dependencies": { "dotenv": "*" } }, "sha512-g2FXjlDX/cYuc5CiQvyU/6kkbP1JtmGzh0obW50zD7OKeILVL0NSpPWLXVfqoAGQjom2/SLLx9zHq0KXvD6mbw=="],
"@types/email-templates": ["@types/email-templates@10.0.4", "", { "dependencies": { "@types/html-to-text": "*", "@types/nodemailer": "*", "juice": "^8.0.0" } }, "sha512-8O2bdGPO6RYgH2DrnFAcuV++s+8KNA5e2Erjl6UxgKRVsBH9zXu2YLrLyOBRMn2VyEYmzgF+6QQUslpVhj0y/g=="],
@ -2707,7 +2703,7 @@
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="],
@ -3117,7 +3113,7 @@
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="],
"tinypool": ["tinypool@1.0.2", "", {}, "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA=="],
@ -3533,10 +3529,10 @@
"@nuxt/kit/pkg-types": ["pkg-types@2.1.0", "", { "dependencies": { "confbox": "^0.2.1", "exsolve": "^1.0.1", "pathe": "^2.0.3" } }, "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A=="],
"@nuxt/kit/tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="],
"@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
"@rollup/pluginutils/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"@selderee/plugin-htmlparser2/domhandler": ["domhandler@4.3.1", "", { "dependencies": { "domelementtype": "^2.2.0" } }, "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ=="],
"@swc/cli/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
@ -3603,8 +3599,6 @@
"ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"apollo-boost/ts-invariant": ["ts-invariant@0.4.4", "", { "dependencies": { "tslib": "^1.9.3" } }, "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA=="],
"apollo-boost/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
@ -3697,8 +3691,6 @@
"database/ts-jest": ["ts-jest@27.0.5", "", { "dependencies": { "bs-logger": "0.x", "fast-json-stable-stringify": "2.x", "jest-util": "^27.0.0", "json5": "2.x", "lodash": "4.x", "make-error": "1.x", "semver": "7.x", "yargs-parser": "20.x" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", "@types/jest": "^27.0.0", "babel-jest": ">=27.0.0 <28", "jest": "^27.0.0", "typescript": ">=3.8 <5.0" }, "optionalPeers": ["@babel/core", "@types/jest", "babel-jest"], "bin": { "ts-jest": "cli.js" } }, "sha512-lIJApzfTaSSbtlksfFNHkWOzLJuuSm4faFAfo5kvzOiRAuoN4/eKxVJ2zEAho8aecE04qX6K1pAzfH5QHL1/8w=="],
"database/vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
"decompress-response/mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
"dht-node/@types/jest": ["@types/jest@27.5.1", "", { "dependencies": { "jest-matcher-utils": "^27.0.0", "pretty-format": "^27.0.0" } }, "sha512-fUy7YRpT+rHXto1YlL+J9rs0uLGyiqVt3ZOTQR+4ROc47yNl8WLdVLgUloBRhOxP1PZvguHl44T3H0wAWxahYQ=="],
@ -3747,6 +3739,8 @@
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"fdir/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"federation/apollo-server-testing": ["apollo-server-testing@2.25.2", "", { "dependencies": { "apollo-server-core": "^2.25.2" }, "peerDependencies": { "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" } }, "sha512-HjQV9wPbi/ZqpRbyyhNwCbaDnfjDM0hTRec5TOoOjurEZ/vh4hTPHwGkDZx3kbcWowhGxe2qoHM6KANSB/SxuA=="],
"federation/helmet": ["helmet@7.2.0", "", {}, "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw=="],
@ -3825,8 +3819,6 @@
"jest-util/@types/node": ["@types/node@18.19.96", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-PzBvgsZ7YdFs/Kng1BSW8IGv68/SPcOxYYhT7luxD7QyzIhFS1xPTpfK3K9eHBa7hVwlW+z8nN0mOd515yaduQ=="],
"jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"jest-watcher/@types/node": ["@types/node@18.19.96", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-PzBvgsZ7YdFs/Kng1BSW8IGv68/SPcOxYYhT7luxD7QyzIhFS1xPTpfK3K9eHBa7hVwlW+z8nN0mOd515yaduQ=="],
"jest-worker/@types/node": ["@types/node@18.19.96", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-PzBvgsZ7YdFs/Kng1BSW8IGv68/SPcOxYYhT7luxD7QyzIhFS1xPTpfK3K9eHBa7hVwlW+z8nN0mOd515yaduQ=="],
@ -3849,8 +3841,6 @@
"mailparser/tlds": ["tlds@1.255.0", "", { "bin": { "tlds": "bin.js" } }, "sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"multimatch/@types/minimatch": ["@types/minimatch@3.0.5", "", {}, "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ=="],
@ -3967,6 +3957,8 @@
"test-exclude/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"ts-jest/jest": ["jest@27.5.1", "", { "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", "jest-cli": "^27.5.1" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "optionalPeers": ["node-notifier"], "bin": { "jest": "bin/jest.js" } }, "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ=="],
"ts-jest/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
@ -3997,14 +3989,16 @@
"unimport/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"unimport/pkg-types": ["pkg-types@2.1.0", "", { "dependencies": { "confbox": "^0.2.1", "exsolve": "^1.0.1", "pathe": "^2.0.3" } }, "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A=="],
"unimport/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"unimport/tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="],
"unimport/pkg-types": ["pkg-types@2.1.0", "", { "dependencies": { "confbox": "^0.2.1", "exsolve": "^1.0.1", "pathe": "^2.0.3" } }, "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A=="],
"unimport/unplugin": ["unplugin@2.3.2", "", { "dependencies": { "acorn": "^8.14.1", "picomatch": "^4.0.2", "webpack-virtual-modules": "^0.6.2" } }, "sha512-3n7YA46rROb3zSj8fFxtxC/PqoyvYQ0llwz9wtUPUutr9ig09C8gGo5CWCwHrUzlqC1LLR43kxp5vEIyH1ac1w=="],
"unplugin-utils/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"unplugin-utils/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"unplugin-vue-components/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"unplugin-vue-components/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
@ -4127,30 +4121,6 @@
"database/ts-jest/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
"database/vitest/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
"database/vitest/@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="],
"database/vitest/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
"database/vitest/@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="],
"database/vitest/@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="],
"database/vitest/@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
"database/vitest/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
"database/vitest/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"database/vitest/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"database/vitest/tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
"database/vitest/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
"database/vitest/vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
"dht-node/ts-jest/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
"dht-rpc/sodium-universal/sodium-native": ["sodium-native@5.0.1", "", { "dependencies": { "require-addon": "^1.1.0", "which-runtime": "^1.2.1" } }, "sha512-Q305aUXc0OzK7VVRvWkeEQJQIHs6slhFwWpyqLB5iJqhpyt2lYIVu96Y6PQ7TABIlWXVF3YiWDU3xS2Snkus+g=="],
@ -4221,8 +4191,6 @@
"jest-worker/jest-util/@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="],
"jest-worker/jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"js-beautify/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"jsdom/parse5/entities": ["entities@6.0.0", "", {}, "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw=="],
@ -4279,6 +4247,8 @@
"typeorm/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"unctx/unplugin/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"unimport/pkg-types/confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
"unplugin-vue-components/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
@ -4287,8 +4257,6 @@
"unplugin-vue-components/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"vite-plugin-html/@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
"vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
@ -4365,8 +4333,6 @@
"cheerio-select/domutils/dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="],
"chokidar-cli/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"chokidar-cli/yargs/cliui/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="],
"chokidar-cli/yargs/cliui/wrap-ansi": ["wrap-ansi@5.1.0", "", { "dependencies": { "ansi-styles": "^3.2.0", "string-width": "^3.0.0", "strip-ansi": "^5.0.0" } }, "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q=="],
@ -4383,12 +4349,6 @@
"css-select/domutils/dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="],
"database/vitest/@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"database/vitest/@vitest/spy/tinyspy": ["tinyspy@4.0.3", "", {}, "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A=="],
"database/vitest/@vitest/utils/loupe": ["loupe@3.1.4", "", {}, "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg=="],
"editorconfig/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
@ -4413,8 +4373,6 @@
"mailparser/html-to-text/selderee/parseley": ["parseley@0.12.1", "", { "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" } }, "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw=="],
"nodemon/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"run-applescript/execa/cross-spawn/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="],
@ -4431,8 +4389,6 @@
"typeorm/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"unplugin-vue-components/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"unplugin-vue-components/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"vue-apollo/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],

View File

@ -1,6 +1,6 @@
{
"name": "config-schema",
"version": "2.6.0",
"version": "2.6.1",
"description": "Gradido Config for validate config",
"main": "./build/index.js",
"types": "./src/index.ts",

View File

@ -92,7 +92,7 @@ export const GMS_ACTIVE = Joi.boolean()
.required()
export const GDT_ACTIVE = Joi.boolean()
.description('Flag to indicate if the GMS (Geographic Member Search) service is used.')
.description('Flag to indicate if the GDT (Gradido Transform) service is used.')
.default(false)
.required()

View File

@ -1,6 +1,6 @@
{
"name": "core",
"version": "2.6.0",
"version": "2.6.1",
"description": "Gradido Core Code, High-Level Shared Code, with dependencies on other modules",
"main": "./build/index.js",
"types": "./src/index.ts",

View File

@ -15,13 +15,13 @@ export const interpretEncryptedTransferArgs = async (args: EncryptedTransferArgs
// first find with args.publicKey the community 'requestingCom', which starts the request
const requestingCom = await DbCommunity.findOneBy({ publicKey: Buffer.from(args.publicKey, 'hex') })
if (!requestingCom) {
const errmsg = `unknown requesting community with publicKey ${args.publicKey}`
const errmsg = `unknown requesting community with publicKey ${Buffer.from(args.publicKey, 'hex')}`
methodLogger.error(errmsg)
methodLogger.removeContext('handshakeID')
throw new Error(errmsg)
}
if (!requestingCom.publicJwtKey) {
const errmsg = `missing publicJwtKey of requesting community with publicKey ${args.publicKey}`
const errmsg = `missing publicJwtKey of requesting community with publicKey ${Buffer.from(args.publicKey, 'hex')}`
methodLogger.error(errmsg)
methodLogger.removeContext('handshakeID')
throw new Error(errmsg)
@ -31,7 +31,7 @@ export const interpretEncryptedTransferArgs = async (args: EncryptedTransferArgs
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 ${args.publicKey}`
const errmsg = `invalid payload of community with publicKey ${Buffer.from(args.publicKey, 'hex')}`
methodLogger.error(errmsg)
methodLogger.removeContext('handshakeID')
throw new Error(errmsg)

View File

@ -56,27 +56,32 @@ ENV PATH="/root/.bun/bin:${PATH}"
FROM bun-base as installer
COPY --chown=app:app . .
RUN bun install --filter database --production --no-cache --frozen-lockfile
##################################################################################
# Build Shared ###################################################################
##################################################################################
FROM installer as build-shared
RUN bun install --filter shared --no-cache --frozen-lockfile \
&& cd shared && yarn typecheck && yarn build
##################################################################################
# Build ##########################################################################
##################################################################################
FROM installer as build
RUN bun install --no-cache --frozen-lockfile \
&& cd shared && yarn build \
&& cd ../database && yarn build && yarn typecheck
RUN bun install --filter database --production --no-cache --frozen-lockfile
##################################################################################
# PRODUCTION IMAGE ###############################################################
##################################################################################
FROM base as production
COPY --chown=app:app --from=installer ${DOCKER_WORKDIR}/src ./src
COPY --chown=app:app --from=installer ${DOCKER_WORKDIR}/migration ./migration
COPY --chown=app:app --from=installer ${DOCKER_WORKDIR}/node_modules ./node_modules
COPY --chown=app:app --from=installer ${DOCKER_WORKDIR}/package.json ./package.json
COPY --chown=app:app --from=installer ${DOCKER_WORKDIR}/tsconfig.json ./tsconfig.json
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
COPY --chown=app:app --from=build ${DOCKER_WORKDIR}/database ./database
COPY --chown=app:app --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
COPY --chown=app:app --from=build ${DOCKER_WORKDIR}/package.json ./package.json
##################################################################################
# TEST UP ########################################################################
@ -84,7 +89,7 @@ COPY --chown=app:app --from=installer ${DOCKER_WORKDIR}/tsconfig.json ./tsconfig
FROM production as up
# Run command
CMD /bin/sh -c "yarn up"
CMD /bin/sh -c "cd database && yarn up"
##################################################################################
# TEST RESET #####################################################################
@ -92,7 +97,7 @@ CMD /bin/sh -c "yarn up"
FROM production as reset
# Run command
CMD /bin/sh -c "yarn reset"
CMD /bin/sh -c "cd database && yarn reset"
##################################################################################
# TEST DOWN ######################################################################
@ -100,4 +105,4 @@ CMD /bin/sh -c "yarn reset"
FROM production as down
# Run command
CMD /bin/sh -c "yarn down"
CMD /bin/sh -c "cd database && yarn down"

View File

@ -0,0 +1,14 @@
/* MIGRATION TO ADD hiero topic id IN COMMUNITY TABLE
*
* This migration adds fields for the hiero topic id in the community.table
*/
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
'ALTER TABLE `communities` ADD COLUMN `hiero_topic_id` varchar(512) DEFAULT NULL AFTER `location`;',
)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn('ALTER TABLE `communities` DROP COLUMN `hiero_topic_id`;')
}

View File

@ -0,0 +1,20 @@
/* MIGRATION TO INCREASE memo TO 512 in all tables which have a memo field
*
* This migration increases the memo field in all tables which have a memo field to 512
*/
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn('ALTER TABLE `contributions` MODIFY COLUMN `memo` varchar(512) NOT NULL;')
await queryFn('ALTER TABLE `contribution_links` MODIFY COLUMN `memo` varchar(512) NOT NULL;')
await queryFn('ALTER TABLE `pending_transactions` MODIFY COLUMN `memo` varchar(512) NOT NULL;')
await queryFn('ALTER TABLE `transactions` MODIFY COLUMN `memo` varchar(512) NOT NULL;')
await queryFn('ALTER TABLE `transaction_links` MODIFY COLUMN `memo` varchar(512) NOT NULL;')
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn('ALTER TABLE `contributions` MODIFY COLUMN `memo` varchar(255) NOT NULL;')
await queryFn('ALTER TABLE `contribution_links` MODIFY COLUMN `memo` varchar(255) NOT NULL;')
await queryFn('ALTER TABLE `pending_transactions` MODIFY COLUMN `memo` varchar(255) NOT NULL;')
await queryFn('ALTER TABLE `transactions` MODIFY COLUMN `memo` varchar(255) NOT NULL;')
await queryFn('ALTER TABLE `transaction_links` MODIFY COLUMN `memo` varchar(255) NOT NULL;')
}

View File

@ -1,6 +1,6 @@
{
"name": "database",
"version": "2.6.0",
"version": "2.6.1",
"description": "Gradido Database Tool to execute database migrations",
"main": "./build/index.js",
"types": "./src/index.ts",
@ -46,7 +46,7 @@
"ts-jest": "27.0.5",
"ts-node": "^10.9.2",
"typescript": "^4.9.5",
"vitest": "^3.2.4"
"vitest": "^2.0.5"
},
"dependencies": {
"@types/uuid": "^8.3.4",

View File

@ -30,6 +30,10 @@ export class Community extends BaseEntity {
@Column({ name: 'private_key', type: 'binary', length: 64, nullable: true })
privateKey: Buffer | null
/**
* Most of time a uuidv4 value, but could be also a uint32 number for a short amount of time, so please check before use
* in community authentication this field is used to store a oneTimePassCode (uint32 number)
*/
@Column({
name: 'community_uuid',
type: 'char',
@ -69,6 +73,9 @@ export class Community extends BaseEntity {
})
location: Geometry | null
@Column({ name: 'hiero_topic_id', type: 'varchar', length: 255, nullable: true })
hieroTopicId: string | null
@CreateDateColumn({
name: 'created_at',
type: 'datetime',

View File

@ -39,7 +39,7 @@ export class Contribution extends BaseEntity {
@Column({ type: 'datetime', nullable: false, name: 'contribution_date' })
contributionDate: Date
@Column({ type: 'varchar', length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
@Column({ type: 'varchar', length: 512, nullable: false, collation: 'utf8mb4_unicode_ci' })
memo: string
@Column({

View File

@ -10,7 +10,7 @@ export class ContributionLink extends BaseEntity {
@Column({ type: 'varchar', length: 100, nullable: false, collation: 'utf8mb4_unicode_ci' })
name: string
@Column({ type: 'varchar', length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
@Column({ type: 'varchar', length: 512, nullable: false, collation: 'utf8mb4_unicode_ci' })
memo: string
@Column({ name: 'valid_from', type: 'datetime', nullable: false })

View File

@ -71,7 +71,7 @@ export class PendingTransaction extends BaseEntity {
})
decayStart: Date | null
@Column({ type: 'varchar', length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
@Column({ type: 'varchar', length: 512, nullable: false, collation: 'utf8mb4_unicode_ci' })
memo: string
@Column({ name: 'creation_date', type: 'datetime', precision: 3, nullable: true, default: null })

View File

@ -71,7 +71,7 @@ export class Transaction extends BaseEntity {
})
decayStart: Date | null
@Column({ type: 'varchar', length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
@Column({ type: 'varchar', length: 512, nullable: false, collation: 'utf8mb4_unicode_ci' })
memo: string
@Column({ name: 'creation_date', type: 'datetime', precision: 3, nullable: true, default: null })

View File

@ -32,7 +32,7 @@ export class TransactionLink extends BaseEntity {
})
holdAvailableAmount: Decimal
@Column({ type: 'varchar', length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
@Column({ type: 'varchar', length: 512, nullable: false, collation: 'utf8mb4_unicode_ci' })
memo: string
@Column({ type: 'varchar', length: 24, nullable: false, collation: 'utf8mb4_unicode_ci' })

View File

@ -49,14 +49,15 @@ export const findUserByIdentifier = async (
const user = userContact.user
user.emailContact = userContact
return user
}
}
} else if (aliasSchema.safeParse(identifier).success) {
return await DbUser.findOne({
where: { alias: identifier, community: communityWhere },
relations: ['emailContact', 'community'],
})
} else {
// should don't happen often, so we create only in the rare case a logger for it
getLogger(`${LOG4JS_QUERIES_CATEGORY_NAME}.user.findUserByIdentifier`).warn('Unknown identifier type', identifier)
}
// should don't happen often, so we create only in the rare case a logger for it
getLogger(`${LOG4JS_QUERIES_CATEGORY_NAME}.user.findUserByIdentifier`).warn('Unknown identifier type', identifier)
return null
}

View File

@ -129,6 +129,42 @@ server {
error_log $GRADIDO_LOG_PATH/nginx-error.hooks.log warn;
}
# Well-Known for openid connect
location /.well-known/ {
limit_req zone=backend burst=10 nodelay;
limit_conn addr 5;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:4000/realms/gradido/.well-known;
proxy_redirect off;
access_log $GRADIDO_LOG_PATH/nginx-access.well-known.log gradido_log;
error_log $GRADIDO_LOG_PATH/nginx-error.well-known.log warn;
}
# Well-Known for openid connect
location /realms/gradido {
limit_req zone=backend burst=10 nodelay;
limit_conn addr 5;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:4000/realms/gradido;
proxy_redirect off;
access_log $GRADIDO_LOG_PATH/nginx-access.well-known.log gradido_log;
error_log $GRADIDO_LOG_PATH/nginx-error.well-known.log warn;
}
# Admin Frontend
location /admin {
limit_req zone=frontend burst=30 nodelay;

View File

@ -114,6 +114,42 @@ server {
error_log $GRADIDO_LOG_PATH/nginx-error.hooks.log warn;
}
# Well-Known for openid connect
location /.well-known/ {
limit_req zone=backend burst=10 nodelay;
limit_conn addr 5;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:4000/realms/gradido/.well-known;
proxy_redirect off;
access_log $GRADIDO_LOG_PATH/nginx-access.well-known.log gradido_log;
error_log $GRADIDO_LOG_PATH/nginx-error.well-known.log warn;
}
# Well-Known for openid connect
location /realms/gradido {
limit_req zone=backend burst=10 nodelay;
limit_conn addr 5;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:4000/realms/gradido;
proxy_redirect off;
access_log $GRADIDO_LOG_PATH/nginx-access.well-known.log gradido_log;
error_log $GRADIDO_LOG_PATH/nginx-error.well-known.log warn;
}
# Admin Frontend
location /admin {
limit_req zone=frontend burst=30 nodelay;

View File

@ -1,6 +1,6 @@
{
"name": "dht-node",
"version": "2.6.0",
"version": "2.6.1",
"description": "Gradido dht-node module",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/",

View File

@ -48,7 +48,7 @@ export const startDHT = async (topic: string): Promise<void> => {
) as KeyPair
const pubKeyString = keyPair.publicKey.toString('hex')
logger.info(`keyPairDHT: publicKey=${pubKeyString}`)
logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`)
logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex').slice(0, 6)}`)
await writeHomeCommunityEntry(keyPair)
const ownApiVersions = await writeFederatedHomeCommunityEntries(pubKeyString)

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
{
"name": "dlt-connector",
"repository": "git@github.com:gradido/gradido.git",
"version": "2.6.1",
"description": "Gradido DLT-Connector",
"author": "Gradido Academy - https://www.gradido.net",
"license": "Apache-2.0",
"version": "1.0.50",
"private": true,
"scripts": {
"start": "bun run src/index.ts",

1907
e2e-tests/bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "typescript",
"devDependencies": {
"@playwright/test": "^1.53.1",
"@types/node": "^24.0.7",
},
},
},
"packages": {
"@playwright/test": ["@playwright/test@1.54.2", "", { "dependencies": { "playwright": "1.54.2" }, "bin": { "playwright": "cli.js" } }, "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA=="],
"@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="],
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
"playwright": ["playwright@1.54.2", "", { "dependencies": { "playwright-core": "1.54.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw=="],
"playwright-core": ["playwright-core@1.54.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA=="],
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
}
}

View File

@ -1,6 +1,6 @@
{
"name": "federation",
"version": "2.6.0",
"version": "2.6.1",
"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",

View File

@ -31,7 +31,7 @@ export class AuthenticationClient {
methodLogger.addContext('handshakeID', args.handshakeID)
methodLogger.debug('openConnectionCallback with endpoint', this.endpoint, args)
try {
const { data } = await this.client.rawRequest<any>(openConnectionCallback, { args })
const { data } = await this.client.rawRequest<{ openConnectionCallback: boolean }>(openConnectionCallback, { args })
methodLogger.debug('after openConnectionCallback: data:', data)
if (!data || !data.openConnectionCallback) {
@ -51,13 +51,13 @@ export class AuthenticationClient {
methodLogger.addContext('handshakeID', args.handshakeID)
methodLogger.debug('authenticate with endpoint=', this.endpoint)
try {
const { data } = await this.client.rawRequest<any>(authenticate, { args })
const { data } = await this.client.rawRequest<{ authenticate: string }>(authenticate, { args })
methodLogger.debug('after authenticate: data:', data)
const authUuid: string = data?.authenticate
if (authUuid) {
methodLogger.debug('received authenticated uuid', authUuid)
return authUuid
const responseJwt = data?.authenticate
if (responseJwt) {
methodLogger.debug('received authenticated uuid as jwt', responseJwt)
return responseJwt
}
} catch (err) {
methodLogger.error('authenticate failed', {

View File

@ -9,11 +9,11 @@ import {
getHomeCommunity,
} from 'database'
import { getLogger } from 'log4js'
import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, OpenConnectionJwtPayloadType } from 'shared'
import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, OpenConnectionJwtPayloadType, uint32Schema, uuidv4Schema } from 'shared'
import { Arg, Mutation, Resolver } from 'type-graphql'
import { startAuthentication, startOpenConnectionCallback } from '../util/authenticateCommunity'
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver`)
const createLogger = (method: string ) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.${method}`)
@Resolver()
export class AuthenticationResolver {
@ -22,45 +22,49 @@ export class AuthenticationResolver {
@Arg('data')
args: EncryptedTransferArgs,
): Promise<boolean> {
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.openConnection`)
const methodLogger = createLogger('openConnection')
methodLogger.addContext('handshakeID', args.handshakeID)
methodLogger.debug(`openConnection() via apiVersion=1_0:`, args)
const openConnectionJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionJwtPayloadType
methodLogger.debug('openConnectionJwtPayload', openConnectionJwtPayload)
if (!openConnectionJwtPayload) {
const errmsg = `invalid OpenConnection payload of requesting community with publicKey` + args.publicKey
methodLogger.error(errmsg)
methodLogger.removeContext('handshakeID')
throw new Error(errmsg)
}
if (openConnectionJwtPayload.tokentype !== OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE) {
const errmsg = `invalid tokentype of community with publicKey` + args.publicKey
methodLogger.error(errmsg)
methodLogger.removeContext('handshakeID')
throw new Error(errmsg)
}
if (!openConnectionJwtPayload.url) {
const errmsg = `invalid url of community with publicKey` + args.publicKey
methodLogger.error(errmsg)
methodLogger.removeContext('handshakeID')
throw new Error(errmsg)
}
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))
if (!openConnectionJwtPayload.url.startsWith(fedComA.endPoint)) {
const errmsg = `invalid url of community with publicKey` + args.publicKey
methodLogger.error(errmsg)
methodLogger.removeContext('handshakeID')
throw new Error(errmsg)
}
try {
const openConnectionJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionJwtPayloadType
methodLogger.debug('openConnectionJwtPayload', openConnectionJwtPayload)
if (!openConnectionJwtPayload) {
const errmsg = `invalid OpenConnection payload of requesting community with publicKey` + args.publicKey
methodLogger.error(errmsg)
// no infos to the caller
return true
}
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
}
if (!openConnectionJwtPayload.url) {
const errmsg = `invalid url of community with publicKey` + args.publicKey
methodLogger.error(errmsg)
// no infos to the caller
return true
}
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))
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
}
// no await to respond immediately and invoke callback-request asynchronously
void startOpenConnectionCallback(args.handshakeID, args.publicKey, CONFIG.FEDERATION_API)
methodLogger.debug('openConnection() successfully initiated callback and returns true immediately...')
methodLogger.removeContext('handshakeID')
return true
// no await to respond immediately and invoke callback-request asynchronously
void startOpenConnectionCallback(args.handshakeID, args.publicKey, CONFIG.FEDERATION_API)
methodLogger.debug('openConnection() successfully initiated callback and returns true immediately...')
return true
} catch (err) {
methodLogger.error('invalid jwt token:', err)
return true
}
}
@Mutation(() => Boolean)
@ -68,37 +72,41 @@ export class AuthenticationResolver {
@Arg('data')
args: EncryptedTransferArgs,
): Promise<boolean> {
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.openConnectionCallback`)
const methodLogger = createLogger('openConnectionCallback')
methodLogger.addContext('handshakeID', args.handshakeID)
methodLogger.debug(`openConnectionCallback() via apiVersion=1_0 ...`, args)
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)
methodLogger.removeContext('handshakeID')
throw new Error(errmsg)
}
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
}
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 fedComB = await DbFedCommunity.findOneBy({ endPoint, apiVersion })
if (!fedComB) {
const errmsg = `unknown callback community with url` + openConnectionCallbackJwtPayload.url
methodLogger.error(errmsg)
methodLogger.removeContext('handshakeID')
throw new Error(errmsg)
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 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
}
methodLogger.debug(
`found fedComB and start authentication:`,
new FederatedCommunityLoggingView(fedComB),
)
// 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...')
return true
} catch (err) {
methodLogger.error('invalid jwt token:', err)
return true
}
methodLogger.debug(
`found fedComB and start authentication:`,
new FederatedCommunityLoggingView(fedComB),
)
// 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.removeContext('handshakeID')
return true
}
@Mutation(() => String)
@ -106,32 +114,54 @@ export class AuthenticationResolver {
@Arg('data')
args: EncryptedTransferArgs,
): Promise<string | null> {
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.authenticate`)
const methodLogger = createLogger('authenticate')
methodLogger.addContext('handshakeID', args.handshakeID)
methodLogger.debug(`authenticate() via apiVersion=1_0 ...`, args)
const authArgs = await interpretEncryptedTransferArgs(args) as AuthenticationJwtPayloadType
if (!authArgs) {
const errmsg = `invalid authentication payload of requesting community with publicKey` + args.publicKey
methodLogger.error(errmsg)
methodLogger.removeContext('handshakeID')
throw new Error(errmsg)
}
const authCom = await DbCommunity.findOneByOrFail({ communityUuid: authArgs.oneTimeCode })
methodLogger.debug('found authCom:', new CommunityLoggingView(authCom))
if (authCom) {
authCom.communityUuid = authArgs.uuid
authCom.authenticatedAt = new Date()
await DbCommunity.save(authCom)
methodLogger.debug('store authCom.uuid successfully:', new CommunityLoggingView(authCom))
const homeComB = await getHomeCommunity()
if (homeComB?.communityUuid) {
const responseArgs = new AuthenticationResponseJwtPayloadType(args.handshakeID,homeComB.communityUuid)
const responseJwt = await encryptAndSign(responseArgs, homeComB.privateJwtKey!, authCom.publicJwtKey!)
methodLogger.removeContext('handshakeID')
return responseJwt
try {
const authArgs = await interpretEncryptedTransferArgs(args) as AuthenticationJwtPayloadType
if (!authArgs) {
const errmsg = `invalid authentication payload of requesting community with publicKey` + args.publicKey
methodLogger.error(errmsg)
// no infos to the caller
return null
}
if (!uint32Schema.safeParse(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 authCom = await DbCommunity.findOneByOrFail({ communityUuid: authArgs.oneTimeCode })
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
}
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
}
authCom.communityUuid = communityUuid.data
authCom.authenticatedAt = new Date()
await DbCommunity.save(authCom)
methodLogger.debug('store authCom.uuid successfully:', new CommunityLoggingView(authCom))
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
}
}
return null
} catch (err) {
methodLogger.error('invalid jwt token:', err)
return null
}
methodLogger.removeContext('handshakeID')
return null
}
}

View File

@ -8,8 +8,8 @@ import Decimal from 'decimal.js-light'
import { GraphQLError } from 'graphql'
import { getLogger } from 'log4js'
import { DataSource } from 'typeorm'
import { SendCoinsArgs } from '../model/SendCoinsArgs'
import { EncryptedTransferArgs } from 'core'
import { createKeyPair, encryptAndSign, SendCoinsJwtPayloadType, SendCoinsResponseJwtPayloadType, verifyAndDecrypt } from 'shared'
let mutate: ApolloServerTestClient['mutate'] // , con: Connection
// let query: ApolloServerTestClient['query']
@ -21,8 +21,8 @@ let testEnv: {
CONFIG.FEDERATION_API = '1_0'
let homeCom: DbCommunity
let foreignCom: DbCommunity
let recipientCom: DbCommunity
let senderCom: DbCommunity
let sendUser: DbUser
let sendContact: DbUserContact
let recipUser: DbUser
@ -37,7 +37,7 @@ beforeAll(async () => {
})
afterAll(async () => {
// await cleanDB()
await cleanDB()
if (testEnv.con?.isInitialized) {
await testEnv.con.destroy()
}
@ -45,53 +45,54 @@ afterAll(async () => {
describe('SendCoinsResolver', () => {
const voteForSendCoinsMutation = `
mutation ($args: SendCoinsArgs!) {
voteForSendCoins(data: $args) {
vote
recipGradidoID
recipFirstName
recipLastName
recipAlias
}
mutation ($args: EncryptedTransferArgs!) {
voteForSendCoins(data: $args)
}`
const settleSendCoinsMutation = `
mutation ($args: SendCoinsArgs!) {
mutation ($args: EncryptedTransferArgs!) {
settleSendCoins(data: $args)
}`
const revertSendCoinsMutation = `
mutation ($args: SendCoinsArgs!) {
mutation ($args: EncryptedTransferArgs!) {
revertSendCoins(data: $args)
}`
const revertSettledSendCoinsMutation = `
mutation ($args: SendCoinsArgs!) {
mutation ($args: EncryptedTransferArgs!) {
revertSettledSendCoins(data: $args)
}`
beforeEach(async () => {
await cleanDB()
homeCom = DbCommunity.create()
homeCom.foreign = false
homeCom.url = 'homeCom-url'
homeCom.name = 'homeCom-Name'
homeCom.description = 'homeCom-Description'
homeCom.creationDate = new Date()
homeCom.publicKey = Buffer.from('homeCom-publicKey')
homeCom.communityUuid = '56a55482-909e-46a4-bfa2-cd025e894eba'
await DbCommunity.insert(homeCom)
// Generate key pair using jose library
const { publicKey: homePublicKey, privateKey: homePrivateKey } = await createKeyPair();
recipientCom = DbCommunity.create()
recipientCom.foreign = false
recipientCom.url = 'homeCom-url'
recipientCom.name = 'homeCom-Name'
recipientCom.description = 'homeCom-Description'
recipientCom.creationDate = new Date()
recipientCom.publicKey = Buffer.alloc(32, '15F92F8EC2EA685D5FD51EE3588F5B4805EBD330EF9EDD16043F3BA9C35C0D91', 'hex') // 'homeCom-publicKey', 'hex')
recipientCom.publicJwtKey = homePublicKey;
recipientCom.privateJwtKey = homePrivateKey;
recipientCom.communityUuid = '56a55482-909e-46a4-bfa2-cd025e894eba'
await DbCommunity.insert(recipientCom)
foreignCom = DbCommunity.create()
foreignCom.foreign = true
foreignCom.url = 'foreignCom-url'
foreignCom.name = 'foreignCom-Name'
foreignCom.description = 'foreignCom-Description'
foreignCom.creationDate = new Date()
foreignCom.publicKey = Buffer.from('foreignCom-publicKey')
foreignCom.communityUuid = '56a55482-909e-46a4-bfa2-cd025e894ebb'
await DbCommunity.insert(foreignCom)
const { publicKey: foreignPublicKey, privateKey: foreignPrivateKey } = await createKeyPair();
senderCom = DbCommunity.create()
senderCom.foreign = true
senderCom.url = 'foreignCom-url'
senderCom.name = 'foreignCom-Name'
senderCom.description = 'foreignCom-Description'
senderCom.creationDate = new Date()
senderCom.publicKey = Buffer.alloc(32, '15F92F8EC2EA685D5FD51EE3588F5B4805EBD330EF9EDD16043F3BA9C35C0D92', 'hex') // 'foreignCom-publicKey', 'hex')
senderCom.publicJwtKey = foreignPublicKey;
senderCom.privateJwtKey = foreignPrivateKey;
senderCom.communityUuid = '56a55482-909e-46a4-bfa2-cd025e894ebb'
await DbCommunity.insert(senderCom)
sendUser = DbUser.create()
sendUser.alias = 'sendUser-alias'
sendUser.communityUuid = '56a55482-909e-46a4-bfa2-cd025e894eba'
sendUser.communityUuid = '56a55482-909e-46a4-bfa2-cd025e894ebb'
sendUser.firstName = 'sendUser-FirstName'
sendUser.gradidoID = '56a55482-909e-46a4-bfa2-cd025e894ebc'
sendUser.lastName = 'sendUser-LastName'
@ -106,7 +107,7 @@ describe('SendCoinsResolver', () => {
recipUser = DbUser.create()
recipUser.alias = 'recipUser-alias'
recipUser.communityUuid = '56a55482-909e-46a4-bfa2-cd025e894ebb'
recipUser.communityUuid = '56a55482-909e-46a4-bfa2-cd025e894eba'
recipUser.firstName = 'recipUser-FirstName'
recipUser.gradidoID = '56a55482-909e-46a4-bfa2-cd025e894ebd'
recipUser.lastName = 'recipUser-LastName'
@ -120,30 +121,37 @@ describe('SendCoinsResolver', () => {
await DbUser.save(recipUser)
})
describe('voteForSendCoins', () => {
describe('voteForSendCoins', () => {
describe('unknown recipient community', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
args.recipientCommunityUuid = 'invalid foreignCom'
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = new Date().toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
args.senderAlias = sendUser.alias
const payload = new SendCoinsJwtPayloadType(
'handshakeID',
'invalid recipientCom',
recipUser.gradidoID,
new Date().toISOString(),
new Decimal(100),
'X-Com-TX memo',
senderCom.communityUuid!,
sendUser.gradidoID,
fullName(sendUser.firstName, sendUser.lastName),
sendUser.alias
)
// invoke encryption as beeing on the foreignCom side to find in voteForSendCoins the correct homeCom
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, recipientCom.publicJwtKey!)
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = 'handshakeID'
const graphQLResponse = await mutate({
mutation: voteForSendCoinsMutation,
variables: { args },
})
expect(
await mutate({
mutation: voteForSendCoinsMutation,
variables: { args },
}),
graphQLResponse,
).toEqual(
expect.objectContaining({
errors: [new GraphQLError('voteForSendCoins with wrong recipientCommunityUuid')],
errors: [new GraphQLError('voteForSendCoins with wrong recipientCommunityUuid: invalid recipientCom')],
}),
)
})
@ -152,20 +160,25 @@ describe('SendCoinsResolver', () => {
describe('unknown recipient user', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = 'invalid recipient'
args.creationDate = new Date().toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
args.senderAlias = sendUser.alias
const payload = new SendCoinsJwtPayloadType(
'handshakeID',
recipientCom.communityUuid!,
'invalid recipient',
new Date().toISOString(),
new Decimal(100),
'X-Com-TX memo',
senderCom.communityUuid!,
sendUser.gradidoID,
fullName(sendUser.firstName, sendUser.lastName),
sendUser.alias
)
// invoke encryption as beeing on the foreignCom side to find in voteForSendCoins the correct homeCom
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, recipientCom.publicJwtKey!)
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = 'handshakeID'
expect(
await mutate({
mutation: voteForSendCoinsMutation,
@ -175,7 +188,7 @@ describe('SendCoinsResolver', () => {
expect.objectContaining({
errors: [
new GraphQLError(
'voteForSendCoins with unknown recipientUserIdentifier in the community=',
'voteForSendCoins with unknown recipientUserIdentifier in the community=homeCom-Name',
),
],
}),
@ -186,36 +199,42 @@ describe('SendCoinsResolver', () => {
describe('valid X-Com-TX voted per gradidoID', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = new Date().toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
args.senderAlias = sendUser.alias
const payload = new SendCoinsJwtPayloadType(
'handshakeID',
recipientCom.communityUuid!,
recipUser.gradidoID,
new Date().toISOString(),
new Decimal(100),
'X-Com-TX memo',
senderCom.communityUuid!,
sendUser.gradidoID,
fullName(sendUser.firstName, sendUser.lastName),
sendUser.alias
)
// invoke encryption as beeing on the foreignCom side to find in voteForSendCoins the correct homeCom
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, recipientCom.publicJwtKey!)
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = 'handshakeID'
const responseJwt = await mutate({
mutation: voteForSendCoinsMutation,
variables: { args },
})
const voteResult = await verifyAndDecrypt('handshakeID', responseJwt.data.voteForSendCoins, senderCom.privateJwtKey!, recipientCom.publicJwtKey!) as SendCoinsResponseJwtPayloadType
expect(
await mutate({
mutation: voteForSendCoinsMutation,
variables: { args },
}),
voteResult,
).toEqual(
expect.objectContaining({
data: {
voteForSendCoins: {
recipGradidoID: '56a55482-909e-46a4-bfa2-cd025e894ebd',
recipFirstName: 'recipUser-FirstName',
recipLastName: 'recipUser-LastName',
recipAlias: 'recipUser-alias',
vote: true,
},
},
expiration: '10m',
handshakeID: 'handshakeID',
recipGradidoID: '56a55482-909e-46a4-bfa2-cd025e894ebd',
recipFirstName: 'recipUser-FirstName',
recipLastName: 'recipUser-LastName',
recipAlias: 'recipUser-alias',
tokentype: SendCoinsResponseJwtPayloadType.SEND_COINS_RESPONSE_TYPE,
vote: true,
}),
)
})
@ -224,36 +243,39 @@ describe('SendCoinsResolver', () => {
describe('valid X-Com-TX voted per alias', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = recipUser.alias
args.creationDate = new Date().toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
args.senderAlias = sendUser.alias
const payload = new SendCoinsJwtPayloadType(
'handshakeID',
recipientCom.communityUuid!,
recipUser.alias,
new Date().toISOString(),
new Decimal(100),
'X-Com-TX memo',
senderCom.communityUuid!,
sendUser.gradidoID,
fullName(sendUser.firstName, sendUser.lastName),
sendUser.alias
)
// invoke encryption as beeing on the foreignCom side to find in voteForSendCoins the correct homeCom
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, recipientCom.publicJwtKey!)
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = 'handshakeID'
const responseJwt = await mutate({
mutation: voteForSendCoinsMutation,
variables: { args },
})
const voteResult = await verifyAndDecrypt('handshakeID', responseJwt.data.voteForSendCoins, senderCom.privateJwtKey!, recipientCom.publicJwtKey!) as SendCoinsResponseJwtPayloadType
expect(
await mutate({
mutation: voteForSendCoinsMutation,
variables: { args },
}),
voteResult,
).toEqual(
expect.objectContaining({
data: {
voteForSendCoins: {
recipGradidoID: '56a55482-909e-46a4-bfa2-cd025e894ebd',
recipFirstName: 'recipUser-FirstName',
recipLastName: 'recipUser-LastName',
recipAlias: 'recipUser-alias',
vote: true,
},
},
recipGradidoID: '56a55482-909e-46a4-bfa2-cd025e894ebd',
recipFirstName: 'recipUser-FirstName',
recipLastName: 'recipUser-LastName',
recipAlias: 'recipUser-alias',
vote: true,
}),
)
})
@ -262,36 +284,40 @@ describe('SendCoinsResolver', () => {
describe('valid X-Com-TX voted per email', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = recipContact.email
args.creationDate = new Date().toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
args.senderAlias = sendUser.alias
const payload = new SendCoinsJwtPayloadType(
'handshakeID',
recipientCom.communityUuid!,
recipContact.email,
new Date().toISOString(),
new Decimal(100),
'X-Com-TX memo',
senderCom.communityUuid!,
sendUser.gradidoID,
fullName(sendUser.firstName, sendUser.lastName),
sendUser.alias
)
// invoke encryption as beeing on the foreignCom side to find in voteForSendCoins the correct homeCom
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, recipientCom.publicJwtKey!)
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = 'handshakeID'
const responseJwt = await mutate({
mutation: voteForSendCoinsMutation,
variables: { args },
})
const voteResult = await verifyAndDecrypt('handshakeID', responseJwt.data.voteForSendCoins, senderCom.privateJwtKey!, recipientCom.publicJwtKey!) as SendCoinsResponseJwtPayloadType
expect(
await mutate({
mutation: voteForSendCoinsMutation,
variables: { args },
}),
voteResult,
).toEqual(
expect.objectContaining({
data: {
voteForSendCoins: {
recipGradidoID: '56a55482-909e-46a4-bfa2-cd025e894ebd',
recipFirstName: 'recipUser-FirstName',
recipLastName: 'recipUser-LastName',
recipAlias: 'recipUser-alias',
vote: true,
},
},
recipGradidoID: '56a55482-909e-46a4-bfa2-cd025e894ebd',
recipFirstName: 'recipUser-FirstName',
recipLastName: 'recipUser-LastName',
recipAlias: 'recipUser-alias',
vote: true,
}),
)
})
@ -302,20 +328,25 @@ describe('SendCoinsResolver', () => {
const creationDate = new Date()
beforeEach(async () => {
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
args.senderAlias = sendUser.alias
const payload = new SendCoinsJwtPayloadType(
'handshakeID',
recipientCom.communityUuid!,
recipUser.gradidoID,
creationDate.toISOString(),
new Decimal(100),
'X-Com-TX memo',
senderCom.communityUuid!,
sendUser.gradidoID,
fullName(sendUser.firstName, sendUser.lastName),
sendUser.alias
)
// invoke encryption as beeing on the foreignCom side to find in voteForSendCoins the correct homeCom
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, recipientCom.publicJwtKey!)
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = 'handshakeID'
await mutate({
mutation: voteForSendCoinsMutation,
variables: { args },
@ -325,18 +356,25 @@ describe('SendCoinsResolver', () => {
describe('unknown recipient community', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
args.recipientCommunityUuid = 'invalid foreignCom'
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
args.senderAlias = sendUser.alias
const payload = new SendCoinsJwtPayloadType(
'handshakeID',
'invalid recipientCom',
recipUser.gradidoID,
creationDate.toISOString(),
new Decimal(100),
'X-Com-TX memo',
senderCom.communityUuid!,
sendUser.gradidoID,
fullName(sendUser.firstName, sendUser.lastName),
sendUser.alias
)
// invoke encryption as beeing on the foreignCom side to find in voteForSendCoins the correct homeCom
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, recipientCom.publicJwtKey!)
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = 'handshakeID'
expect(
await mutate({
mutation: revertSendCoinsMutation,
@ -344,7 +382,7 @@ describe('SendCoinsResolver', () => {
}),
).toEqual(
expect.objectContaining({
errors: [new GraphQLError('revertSendCoins with wrong recipientCommunityUuid')],
errors: [new GraphQLError('revertSendCoins with wrong recipientCommunityUuid=invalid recipientCom')],
}),
)
})
@ -353,20 +391,25 @@ describe('SendCoinsResolver', () => {
describe('unknown recipient user', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = 'invalid recipient'
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
args.senderAlias = sendUser.alias
const payload = new SendCoinsJwtPayloadType(
'handshakeID',
recipientCom.communityUuid!,
'invalid recipient',
creationDate.toISOString(),
new Decimal(100),
'X-Com-TX memo',
senderCom.communityUuid!,
sendUser.gradidoID,
fullName(sendUser.firstName, sendUser.lastName),
sendUser.alias
)
// invoke encryption as beeing on the foreignCom side to find in voteForSendCoins the correct homeCom
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, recipientCom.publicJwtKey!)
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = 'handshakeID'
expect(
await mutate({
mutation: revertSendCoinsMutation,
@ -376,7 +419,7 @@ describe('SendCoinsResolver', () => {
expect.objectContaining({
errors: [
new GraphQLError(
'revertSendCoins with unknown recipientUserIdentifier in the community=',
'revertSendCoins with unknown recipientUserIdentifier in the community=homeCom-Name',
),
],
}),
@ -387,20 +430,26 @@ describe('SendCoinsResolver', () => {
describe('valid X-Com-TX reverted', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
args.senderAlias = sendUser.alias
const payload = new SendCoinsJwtPayloadType(
'handshakeID',
recipientCom.communityUuid!,
recipUser.gradidoID,
creationDate.toISOString(),
new Decimal(100),
'X-Com-TX memo',
senderCom.communityUuid!,
sendUser.gradidoID,
fullName(sendUser.firstName, sendUser.lastName),
sendUser.alias
)
// invoke encryption as beeing on the foreignCom side to find in voteForSendCoins the correct homeCom
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, recipientCom.publicJwtKey!)
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = 'handshakeID'
expect(
await mutate({
mutation: revertSendCoinsMutation,
@ -421,20 +470,25 @@ describe('SendCoinsResolver', () => {
const creationDate = new Date()
beforeEach(async () => {
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
args.senderAlias = sendUser.alias
const payload = new SendCoinsJwtPayloadType(
'handshakeID',
recipientCom.communityUuid!,
recipUser.gradidoID,
creationDate.toISOString(),
new Decimal(100),
'X-Com-TX memo',
senderCom.communityUuid!,
sendUser.gradidoID,
fullName(sendUser.firstName, sendUser.lastName),
sendUser.alias
)
// invoke encryption as beeing on the foreignCom side to find in voteForSendCoins the correct homeCom
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, recipientCom.publicJwtKey!)
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = 'handshakeID'
await mutate({
mutation: voteForSendCoinsMutation,
variables: { args },
@ -444,18 +498,24 @@ describe('SendCoinsResolver', () => {
describe('unknown recipient community', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
args.recipientCommunityUuid = 'invalid foreignCom'
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
args.senderAlias = sendUser.alias
const payload = new SendCoinsJwtPayloadType(
'handshakeID',
'invalid recipientCom',
recipUser.gradidoID,
creationDate.toISOString(),
new Decimal(100),
'X-Com-TX memo',
senderCom.communityUuid!,
sendUser.gradidoID,
fullName(sendUser.firstName, sendUser.lastName),
sendUser.alias
)
// invoke encryption as beeing on the foreignCom side to find in voteForSendCoins the correct homeCom
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, recipientCom.publicJwtKey!)
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = 'handshakeID'
expect(
await mutate({
mutation: settleSendCoinsMutation,
@ -463,7 +523,7 @@ describe('SendCoinsResolver', () => {
}),
).toEqual(
expect.objectContaining({
errors: [new GraphQLError('settleSendCoins with wrong recipientCommunityUuid')],
errors: [new GraphQLError('settleSendCoins with wrong recipientCommunityUuid=invalid recipientCom')],
}),
)
})
@ -472,20 +532,24 @@ describe('SendCoinsResolver', () => {
describe('unknown recipient user', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = 'invalid recipient'
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
args.senderAlias = sendUser.alias
const payload = new SendCoinsJwtPayloadType(
'handshakeID',
recipientCom.communityUuid!,
'invalid recipient',
creationDate.toISOString(),
new Decimal(100),
'X-Com-TX memo',
senderCom.communityUuid!,
sendUser.gradidoID,
fullName(sendUser.firstName, sendUser.lastName),
sendUser.alias
)
// invoke encryption as beeing on the foreignCom side to find in voteForSendCoins the correct homeCom
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, recipientCom.publicJwtKey!)
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = 'handshakeID'
expect(
await mutate({
mutation: settleSendCoinsMutation,
@ -495,7 +559,7 @@ describe('SendCoinsResolver', () => {
expect.objectContaining({
errors: [
new GraphQLError(
'settleSendCoins with unknown recipientUserIdentifier in the community=',
'settleSendCoins with unknown recipientUserIdentifier in the community=' + recipientCom.name,
),
],
}),
@ -506,20 +570,24 @@ describe('SendCoinsResolver', () => {
describe('valid X-Com-TX settled', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
args.senderAlias = sendUser.alias
const payload = new SendCoinsJwtPayloadType(
'handshakeID',
recipientCom.communityUuid!,
recipUser.gradidoID,
creationDate.toISOString(),
new Decimal(100),
'X-Com-TX memo',
senderCom.communityUuid!,
sendUser.gradidoID,
fullName(sendUser.firstName, sendUser.lastName),
sendUser.alias
)
// invoke encryption as beeing on the foreignCom side to find in voteForSendCoins the correct homeCom
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, recipientCom.publicJwtKey!)
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = 'handshakeID'
expect(
await mutate({
mutation: settleSendCoinsMutation,
@ -540,20 +608,24 @@ describe('SendCoinsResolver', () => {
const creationDate = new Date()
beforeEach(async () => {
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
args.senderAlias = sendUser.alias
const payload = new SendCoinsJwtPayloadType(
'handshakeID',
recipientCom.communityUuid!,
recipUser.gradidoID,
creationDate.toISOString(),
new Decimal(100),
'X-Com-TX memo',
senderCom.communityUuid!,
sendUser.gradidoID,
fullName(sendUser.firstName, sendUser.lastName),
sendUser.alias
)
// invoke encryption as beeing on the foreignCom side to find in voteForSendCoins the correct homeCom
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, recipientCom.publicJwtKey!)
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = 'handshakeID'
await mutate({
mutation: voteForSendCoinsMutation,
variables: { args },
@ -567,18 +639,24 @@ describe('SendCoinsResolver', () => {
describe('unknown recipient community', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
args.recipientCommunityUuid = 'invalid foreignCom'
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
args.senderAlias = sendUser.alias
const payload = new SendCoinsJwtPayloadType(
'handshakeID',
'invalid recipientCom',
recipUser.gradidoID,
creationDate.toISOString(),
new Decimal(100),
'X-Com-TX memo',
senderCom.communityUuid!,
sendUser.gradidoID,
fullName(sendUser.firstName, sendUser.lastName),
sendUser.alias
)
// invoke encryption as beeing on the foreignCom side to find in voteForSendCoins the correct homeCom
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, recipientCom.publicJwtKey!)
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = 'handshakeID'
expect(
await mutate({
mutation: revertSettledSendCoinsMutation,
@ -586,7 +664,7 @@ describe('SendCoinsResolver', () => {
}),
).toEqual(
expect.objectContaining({
errors: [new GraphQLError('revertSettledSendCoins with wrong recipientCommunityUuid')],
errors: [new GraphQLError('revertSettledSendCoins with wrong recipientCommunityUuid=invalid recipientCom')],
}),
)
})
@ -595,20 +673,24 @@ describe('SendCoinsResolver', () => {
describe('unknown recipient user', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = 'invalid recipient'
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
args.senderAlias = sendUser.alias
const payload = new SendCoinsJwtPayloadType(
'handshakeID',
recipientCom.communityUuid!,
'invalid recipient',
creationDate.toISOString(),
new Decimal(100),
'X-Com-TX memo',
senderCom.communityUuid!,
sendUser.gradidoID,
fullName(sendUser.firstName, sendUser.lastName),
sendUser.alias
)
// invoke encryption as beeing on the foreignCom side to find in voteForSendCoins the correct homeCom
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, recipientCom.publicJwtKey!)
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = 'handshakeID'
expect(
await mutate({
mutation: revertSettledSendCoinsMutation,
@ -618,7 +700,7 @@ describe('SendCoinsResolver', () => {
expect.objectContaining({
errors: [
new GraphQLError(
'revertSettledSendCoins with unknown recipientUserIdentifier in the community=',
'revertSettledSendCoins with unknown recipientUserIdentifier in the community=' + recipientCom.name,
),
],
}),
@ -629,20 +711,24 @@ describe('SendCoinsResolver', () => {
describe('valid X-Com-TX settled', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const args = new SendCoinsArgs()
if (foreignCom.communityUuid) {
args.recipientCommunityUuid = foreignCom.communityUuid
}
args.recipientUserIdentifier = recipUser.gradidoID
args.creationDate = creationDate.toISOString()
args.amount = new Decimal(100)
args.memo = 'X-Com-TX memo'
if (homeCom.communityUuid) {
args.senderCommunityUuid = homeCom.communityUuid
}
args.senderUserUuid = sendUser.gradidoID
args.senderUserName = fullName(sendUser.firstName, sendUser.lastName)
args.senderAlias = sendUser.alias
const payload = new SendCoinsJwtPayloadType(
'handshakeID',
recipientCom.communityUuid!,
recipUser.gradidoID,
creationDate.toISOString(),
new Decimal(100),
'X-Com-TX memo',
senderCom.communityUuid!,
sendUser.gradidoID,
fullName(sendUser.firstName, sendUser.lastName),
sendUser.alias
)
// invoke encryption as beeing on the foreignCom side to find in voteForSendCoins the correct homeCom
const jws = await encryptAndSign(payload, senderCom.privateJwtKey!, recipientCom.publicJwtKey!)
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = 'handshakeID'
expect(
await mutate({
mutation: revertSettledSendCoinsMutation,

View File

@ -10,294 +10,363 @@ import Decimal from 'decimal.js-light'
import { getLogger } from 'log4js'
import { Arg, Mutation, Resolver } from 'type-graphql'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { PendingTransactionState } from 'shared'
import { encryptAndSign, PendingTransactionState, verifyAndDecrypt } from 'shared'
import { TransactionTypeId } from '../enum/TransactionTypeId'
import { SendCoinsArgsLoggingView } from '../logger/SendCoinsArgsLogging.view'
import { SendCoinsArgs } from '../model/SendCoinsArgs'
import { SendCoinsResult } from '../model/SendCoinsResult'
import { SendCoinsResponseJwtPayloadType } from 'shared'
import { calculateRecipientBalance } from '../util/calculateRecipientBalance'
// import { checkTradingLevel } from '@/graphql/util/checkTradingLevel'
import { revertSettledReceiveTransaction } from '../util/revertSettledReceiveTransaction'
import { settlePendingReceiveTransaction } from '../util/settlePendingReceiveTransaction'
import { storeForeignUser } from '../util/storeForeignUser'
import { countOpenPendingTransactions } from 'database'
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.SendCoinsResolver`)
import { EncryptedTransferArgs } from 'core'
import { interpretEncryptedTransferArgs } from 'core'
import { SendCoinsJwtPayloadType } from 'shared'
const createLogger = (method: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.SendCoinsResolver.${method}`)
@Resolver()
export class SendCoinsResolver {
@Mutation(() => SendCoinsResult)
@Mutation(() => String)
async voteForSendCoins(
@Arg('data')
args: SendCoinsArgs,
): Promise<SendCoinsResult> {
logger.debug(`voteForSendCoins() via apiVersion=1_0 ...`, new SendCoinsArgsLoggingView(args))
const result = new SendCoinsResult()
args: EncryptedTransferArgs,
): Promise<string> {
const methodLogger = createLogger(`voteForSendCoins`)
methodLogger.addContext('handshakeID', args.handshakeID)
if(methodLogger.isDebugEnabled()) {
methodLogger.debug(`voteForSendCoins() via apiVersion=1_0 ...`, args)
}
const authArgs = await interpretEncryptedTransferArgs(args) as SendCoinsJwtPayloadType
if (!authArgs) {
const errmsg = `invalid authentication payload of requesting community with publicKey` + args.publicKey
methodLogger.error(errmsg)
throw new Error(errmsg)
}
if(methodLogger.isDebugEnabled()) {
methodLogger.debug(`voteForSendCoins() via apiVersion=1_0 ...`, authArgs)
}
// first check if receiver community is correct
const homeCom = await DbCommunity.findOneBy({
communityUuid: args.recipientCommunityUuid,
const recipientCom = await DbCommunity.findOneBy({
communityUuid: authArgs.recipientCommunityUuid,
})
if (!homeCom) {
throw new LogError(
`voteForSendCoins with wrong recipientCommunityUuid`,
args.recipientCommunityUuid,
)
if (!recipientCom) {
const errmsg = `voteForSendCoins with wrong recipientCommunityUuid: ${authArgs.recipientCommunityUuid}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
const senderCom = await DbCommunity.findOneBy({
communityUuid: authArgs.senderCommunityUuid,
})
if (!senderCom) {
const errmsg = `voteForSendCoins with wrong senderCommunityUuid: ${authArgs.senderCommunityUuid}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
let receiverUser
// second check if receiver user exists in this community
receiverUser = await findUserByIdentifier(
args.recipientUserIdentifier,
args.recipientCommunityUuid,
authArgs.recipientUserIdentifier,
authArgs.recipientCommunityUuid,
)
if (!receiverUser) {
logger.error('Error in findUserByIdentifier:')
throw new LogError(
`voteForSendCoins with unknown recipientUserIdentifier in the community=`,
homeCom.name,
)
const errmsg = `voteForSendCoins with unknown recipientUserIdentifier in the community=` + recipientCom.name
methodLogger.error(errmsg)
throw new Error(errmsg)
}
if (await countOpenPendingTransactions([args.senderUserUuid, receiverUser.gradidoID]) > 0) {
throw new LogError(
`There exist still ongoing 'Pending-Transactions' for the involved users on receiver-side!`,
)
if (await countOpenPendingTransactions([authArgs.senderUserUuid, receiverUser.gradidoID]) > 0) {
const errmsg = `There exist still ongoing 'Pending-Transactions' for the involved users on receiver-side!`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
try {
const txDate = new Date(args.creationDate)
const receiveBalance = await calculateRecipientBalance(receiverUser.id, args.amount, txDate)
const txDate = new Date(authArgs.creationDate)
const receiveBalance = await calculateRecipientBalance(receiverUser.id, authArgs.amount, txDate)
const pendingTx = DbPendingTransaction.create()
pendingTx.amount = args.amount
pendingTx.balance = receiveBalance ? receiveBalance.balance : args.amount
pendingTx.amount = authArgs.amount
pendingTx.balance = receiveBalance ? receiveBalance.balance : authArgs.amount
pendingTx.balanceDate = txDate
pendingTx.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0)
pendingTx.decayStart = receiveBalance ? receiveBalance.decay.start : null
pendingTx.creationDate = new Date()
pendingTx.linkedUserCommunityUuid = args.senderCommunityUuid
pendingTx.linkedUserGradidoID = args.senderUserUuid
pendingTx.linkedUserName = args.senderUserName
pendingTx.memo = args.memo
pendingTx.linkedUserCommunityUuid = authArgs.senderCommunityUuid
pendingTx.linkedUserGradidoID = authArgs.senderUserUuid
pendingTx.linkedUserName = authArgs.senderUserName
pendingTx.memo = authArgs.memo
pendingTx.previous = receiveBalance ? receiveBalance.lastTransactionId : null
pendingTx.state = PendingTransactionState.NEW
pendingTx.typeId = TransactionTypeId.RECEIVE
pendingTx.userId = receiverUser.id
pendingTx.userCommunityUuid = args.recipientCommunityUuid
pendingTx.userCommunityUuid = authArgs.recipientCommunityUuid
pendingTx.userGradidoID = receiverUser.gradidoID
pendingTx.userName = fullName(receiverUser.firstName, receiverUser.lastName)
await DbPendingTransaction.insert(pendingTx)
result.vote = true
result.recipFirstName = receiverUser.firstName
result.recipLastName = receiverUser.lastName
result.recipAlias = receiverUser.alias
result.recipGradidoID = receiverUser.gradidoID
logger.debug(`voteForSendCoins()-1_0... successfull`)
const responseArgs = new SendCoinsResponseJwtPayloadType(
authArgs.handshakeID,
true,
receiverUser.gradidoID,
receiverUser.firstName,
receiverUser.lastName,
receiverUser.alias,
)
const responseJwt = await encryptAndSign(responseArgs, recipientCom.privateJwtKey!, senderCom.publicJwtKey!)
methodLogger.debug(`voteForSendCoins()-1_0... successfull`)
return responseJwt
} catch (err) {
throw new LogError(`Error in voteForSendCoins: `, err)
const errmsg = `Error in voteForSendCoins: ` + err
methodLogger.error(errmsg)
throw new Error(errmsg)
}
return result
}
@Mutation(() => Boolean)
async revertSendCoins(
@Arg('data')
args: SendCoinsArgs,
args: EncryptedTransferArgs,
): Promise<boolean> {
logger.debug(`revertSendCoins() via apiVersion=1_0 ...`)
const methodLogger = createLogger(`revertSendCoins`)
methodLogger.addContext('handshakeID', args.handshakeID)
if(methodLogger.isDebugEnabled()) {
methodLogger.debug(`revertSendCoins() via apiVersion=1_0 ...`)
}
const authArgs = await interpretEncryptedTransferArgs(args) as SendCoinsJwtPayloadType
if (!authArgs) {
const errmsg = `invalid revertSendCoins payload of requesting community with publicKey=${args.publicKey}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
if(methodLogger.isDebugEnabled()) {
methodLogger.debug(`revertSendCoins() via apiVersion=1_0 ...`, authArgs)
}
// first check if receiver community is correct
const homeCom = await DbCommunity.findOneBy({
communityUuid: args.recipientCommunityUuid,
communityUuid: authArgs.recipientCommunityUuid,
})
if (!homeCom) {
throw new LogError(
`revertSendCoins with wrong recipientCommunityUuid`,
args.recipientCommunityUuid,
)
const errmsg = `revertSendCoins with wrong recipientCommunityUuid=${authArgs.recipientCommunityUuid}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
let receiverUser
// second check if receiver user exists in this community
receiverUser = await findUserByIdentifier(args.recipientUserIdentifier)
receiverUser = await findUserByIdentifier(authArgs.recipientUserIdentifier)
if (!receiverUser) {
logger.error('Error in findUserByIdentifier')
throw new LogError(
`revertSendCoins with unknown recipientUserIdentifier in the community=`,
homeCom.name,
)
const errmsg = `revertSendCoins with unknown recipientUserIdentifier in the community=${homeCom.name}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
try {
const pendingTx = await DbPendingTransaction.findOneBy({
userCommunityUuid: args.recipientCommunityUuid,
userCommunityUuid: authArgs.recipientCommunityUuid,
userGradidoID: receiverUser.gradidoID,
state: PendingTransactionState.NEW,
typeId: TransactionTypeId.RECEIVE,
balanceDate: new Date(args.creationDate),
linkedUserCommunityUuid: args.senderCommunityUuid,
linkedUserGradidoID: args.senderUserUuid,
balanceDate: new Date(authArgs.creationDate),
linkedUserCommunityUuid: authArgs.senderCommunityUuid,
linkedUserGradidoID: authArgs.senderUserUuid,
})
logger.debug(
'XCom: revertSendCoins found pendingTX=',
pendingTx ? new PendingTransactionLoggingView(pendingTx) : 'null',
)
if (pendingTx && pendingTx.amount.toString() === args.amount.toString()) {
logger.debug('XCom: revertSendCoins matching pendingTX for remove...')
if(methodLogger.isDebugEnabled()) {
methodLogger.debug(
'XCom: revertSendCoins found pendingTX=',
pendingTx ? new PendingTransactionLoggingView(pendingTx) : 'null',
)
}
if (pendingTx && pendingTx.amount.toString() === authArgs.amount.toString()) {
methodLogger.debug('XCom: revertSendCoins matching pendingTX for remove...')
try {
await pendingTx.remove()
logger.debug('XCom: revertSendCoins pendingTX for remove successfully')
methodLogger.debug('XCom: revertSendCoins pendingTX for remove successfully')
} catch (err) {
throw new LogError('Error in revertSendCoins on removing pendingTx of receiver: ', err)
const errmsg = `Error in revertSendCoins on removing pendingTx of receiver: ` + err
methodLogger.error(errmsg)
throw new Error(errmsg)
}
} else {
logger.debug(
methodLogger.debug(
'XCom: revertSendCoins NOT matching pendingTX for remove:',
pendingTx?.amount.toString(),
args.amount.toString(),
authArgs.amount.toString(),
)
throw new LogError(`Can't find in revertSendCoins the pending receiver TX for `, {
args: new SendCoinsArgsLoggingView(args),
const errmsg = `Can't find in revertSendCoins the pending receiver TX for ` + {
args: new SendCoinsArgsLoggingView(authArgs),
pendingTransactionState: PendingTransactionState.NEW,
transactionType: TransactionTypeId.RECEIVE,
})
}
methodLogger.error(errmsg)
throw new Error(errmsg)
}
logger.debug(`revertSendCoins()-1_0... successfull`)
methodLogger.debug(`revertSendCoins()-1_0... successfull`)
return true
} catch (err) {
throw new LogError(`Error in revertSendCoins: `, err)
const errmsg = `Error in revertSendCoins: ` + err
methodLogger.error(errmsg)
throw new Error(errmsg)
}
}
@Mutation(() => Boolean)
async settleSendCoins(
@Arg('data')
args: SendCoinsArgs,
args: EncryptedTransferArgs,
): Promise<boolean> {
logger.debug(`settleSendCoins() via apiVersion=1_0 ...`, new SendCoinsArgsLoggingView(args))
const methodLogger = createLogger(`settleSendCoins`)
methodLogger.addContext('handshakeID', args.handshakeID)
if(methodLogger.isDebugEnabled()) {
methodLogger.debug(`settleSendCoins() via apiVersion=1_0 ...`, args)
}
const authArgs = await interpretEncryptedTransferArgs(args) as SendCoinsJwtPayloadType
if (!authArgs) {
const errmsg = `invalid settleSendCoins payload of requesting community with publicKey=${args.publicKey}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
if(methodLogger.isDebugEnabled()) {
methodLogger.debug(`settleSendCoins() via apiVersion=1_0 ...`, authArgs)
}
// first check if receiver community is correct
const homeCom = await DbCommunity.findOneBy({
communityUuid: args.recipientCommunityUuid,
communityUuid: authArgs.recipientCommunityUuid,
})
if (!homeCom) {
throw new LogError(
`settleSendCoins with wrong recipientCommunityUuid`,
args.recipientCommunityUuid,
)
const errmsg = `settleSendCoins with wrong recipientCommunityUuid=${authArgs.recipientCommunityUuid}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
// second check if receiver user exists in this community
const receiverUser = await findUserByIdentifier(args.recipientUserIdentifier)
const receiverUser = await findUserByIdentifier(authArgs.recipientUserIdentifier)
if (!receiverUser) {
logger.error('Error in findUserByIdentifier')
throw new LogError(
`settleSendCoins with unknown recipientUserIdentifier in the community=`,
homeCom.name,
)
const errmsg = `settleSendCoins with unknown recipientUserIdentifier in the community=${homeCom.name}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
const pendingTx = await DbPendingTransaction.findOneBy({
userCommunityUuid: args.recipientCommunityUuid,
userCommunityUuid: authArgs.recipientCommunityUuid,
userGradidoID: receiverUser.gradidoID,
state: PendingTransactionState.NEW,
typeId: TransactionTypeId.RECEIVE,
balanceDate: new Date(args.creationDate),
linkedUserCommunityUuid: args.senderCommunityUuid,
linkedUserGradidoID: args.senderUserUuid,
balanceDate: new Date(authArgs.creationDate),
linkedUserCommunityUuid: authArgs.senderCommunityUuid,
linkedUserGradidoID: authArgs.senderUserUuid,
})
logger.debug(
'XCom: settleSendCoins found pendingTX=',
pendingTx ? new PendingTransactionLoggingView(pendingTx) : 'null',
)
if(methodLogger.isDebugEnabled()) {
methodLogger.debug(
'XCom: settleSendCoins found pendingTX=',
pendingTx ? new PendingTransactionLoggingView(pendingTx) : 'null',
)
}
if (
pendingTx &&
pendingTx.amount.toString() === args.amount.toString() &&
pendingTx.memo === args.memo
pendingTx.amount.toString() === authArgs.amount.toString() &&
pendingTx.memo === authArgs.memo
) {
logger.debug('XCom: settleSendCoins matching pendingTX for settlement...')
methodLogger.debug('XCom: settleSendCoins matching pendingTX for settlement...')
await settlePendingReceiveTransaction(homeCom, receiverUser, pendingTx)
// after successful x-com-tx store the recipient as foreign user
logger.debug('store recipient as foreign user...')
if (await storeForeignUser(args)) {
logger.info(
methodLogger.debug('store recipient as foreign user...')
if (await storeForeignUser(authArgs)) {
methodLogger.info(
'X-Com: new foreign user inserted successfully...',
args.senderCommunityUuid,
args.senderUserUuid,
authArgs.senderCommunityUuid,
authArgs.senderUserUuid,
)
}
logger.debug(`XCom: settlePendingReceiveTransaction()-1_0... successful`)
methodLogger.debug(`XCom: settlePendingReceiveTransaction()-1_0... successful`)
return true
} else {
logger.debug('XCom: settlePendingReceiveTransaction NOT matching pendingTX for settlement...')
throw new LogError(
`Can't find in settlePendingReceiveTransaction the pending receiver TX for `,
{
args: new SendCoinsArgsLoggingView(args),
pendingTransactionState: PendingTransactionState.NEW,
methodLogger.debug('XCom: settlePendingReceiveTransaction NOT matching pendingTX for settlement...')
const errmsg = `Can't find in settlePendingReceiveTransaction the pending receiver TX for ` + {
args: new SendCoinsArgsLoggingView(authArgs),
pendingTransactionState: PendingTransactionState.NEW,
transactionTypeId: TransactionTypeId.RECEIVE,
},
)
}
methodLogger.error(errmsg)
throw new Error(errmsg)
}
}
@Mutation(() => Boolean)
async revertSettledSendCoins(
@Arg('data')
args: SendCoinsArgs,
args: EncryptedTransferArgs,
): Promise<boolean> {
logger.debug(`revertSettledSendCoins() via apiVersion=1_0 ...`)
const methodLogger = createLogger(`revertSettledSendCoins`)
methodLogger.addContext('handshakeID', args.handshakeID)
if(methodLogger.isDebugEnabled()) {
methodLogger.debug(`revertSettledSendCoins() via apiVersion=1_0 ...`)
}
const authArgs = await interpretEncryptedTransferArgs(args) as SendCoinsJwtPayloadType
if (!authArgs) {
const errmsg = `invalid revertSettledSendCoins payload of requesting community with publicKey=${args.publicKey}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
if(methodLogger.isDebugEnabled()) {
methodLogger.debug(`revertSettledSendCoins() via apiVersion=1_0 ...`, authArgs)
}
// first check if receiver community is correct
const homeCom = await DbCommunity.findOneBy({
communityUuid: args.recipientCommunityUuid,
communityUuid: authArgs.recipientCommunityUuid,
})
if (!homeCom) {
throw new LogError(
`revertSettledSendCoins with wrong recipientCommunityUuid`,
args.recipientCommunityUuid,
)
const errmsg = `revertSettledSendCoins with wrong recipientCommunityUuid=${authArgs.recipientCommunityUuid}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
// second check if receiver user exists in this community
const receiverUser = await findUserByIdentifier(args.recipientUserIdentifier)
const receiverUser = await findUserByIdentifier(authArgs.recipientUserIdentifier)
if (!receiverUser) {
logger.error('Error in findUserByIdentifier')
throw new LogError(
`revertSettledSendCoins with unknown recipientUserIdentifier in the community=`,
homeCom.name,
)
const errmsg = `revertSettledSendCoins with unknown recipientUserIdentifier in the community=${homeCom.name}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
const pendingTx = await DbPendingTransaction.findOneBy({
userCommunityUuid: args.recipientCommunityUuid,
userGradidoID: args.recipientUserIdentifier,
userCommunityUuid: authArgs.recipientCommunityUuid,
userGradidoID: authArgs.recipientUserIdentifier,
state: PendingTransactionState.SETTLED,
typeId: TransactionTypeId.RECEIVE,
balanceDate: new Date(args.creationDate),
linkedUserCommunityUuid: args.senderCommunityUuid,
linkedUserGradidoID: args.senderUserUuid,
balanceDate: new Date(authArgs.creationDate),
linkedUserCommunityUuid: authArgs.senderCommunityUuid,
linkedUserGradidoID: authArgs.senderUserUuid,
})
logger.debug(
methodLogger.debug(
'XCom: revertSettledSendCoins found pendingTX=',
pendingTx ? new PendingTransactionLoggingView(pendingTx) : 'null',
)
if (
pendingTx &&
pendingTx.amount.toString() === args.amount.toString() &&
pendingTx.memo === args.memo
pendingTx.amount.toString() === authArgs.amount.toString() &&
pendingTx.memo === authArgs.memo
) {
logger.debug('XCom: revertSettledSendCoins matching pendingTX for remove...')
methodLogger.debug('XCom: revertSettledSendCoins matching pendingTX for remove...')
try {
await revertSettledReceiveTransaction(homeCom, receiverUser, pendingTx)
logger.debug('XCom: revertSettledSendCoins pendingTX successfully')
methodLogger.debug('XCom: revertSettledSendCoins pendingTX successfully')
} catch (err) {
throw new LogError('Error in revertSettledSendCoins of receiver: ', err)
const errmsg = `Error in revertSettledSendCoins of receiver: ` + err
methodLogger.error(errmsg)
throw new Error(errmsg)
}
} else {
logger.debug('XCom: revertSettledSendCoins NOT matching pendingTX...')
throw new LogError(`Can't find in revertSettledSendCoins the pending receiver TX for `, {
args: new SendCoinsArgsLoggingView(args),
methodLogger.debug('XCom: revertSettledSendCoins NOT matching pendingTX...')
const errmsg = `Can't find in revertSettledSendCoins the pending receiver TX for ` + {
args: new SendCoinsArgsLoggingView(authArgs),
pendingTransactionState: PendingTransactionState.SETTLED,
transactionTypeId: TransactionTypeId.RECEIVE,
})
}
methodLogger.error(errmsg)
throw new Error(errmsg)
}
logger.debug(`revertSendCoins()-1_0... successfull`)
methodLogger.debug(`revertSettledSendCoins()-1_0... successfull`)
return true
}
}

View File

@ -14,7 +14,7 @@ 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, verifyAndDecrypt } from 'shared'
import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, uuidv4Schema, verifyAndDecrypt } from 'shared'
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity`)
@ -40,8 +40,13 @@ export async function startOpenConnectionCallback(
apiVersion: api,
publicKey: comA.publicKey,
})
const oneTimeCode = randombytes_random().toString()
// 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')
}
// TODO: make sure it is unique
const oneTimeCode = randombytes_random().toString()
comA.communityUuid = oneTimeCode
await DbCommunity.save(comA)
methodLogger.debug(

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "frontend",
"version": "2.6.0",
"version": "2.6.1",
"private": true,
"scripts": {
"dev": "concurrently \"yarn watch-scss\" \"vite\"",
@ -82,7 +82,7 @@
"@vue/test-utils": "^2.4.6",
"chokidar-cli": "^3.0.0",
"concurrently": "^9.1.2",
"config-schema": "2.6.0",
"config-schema": "*",
"cross-env": "^7.0.3",
"dotenv-webpack": "^7.0.3",
"eslint": "8.57.1",

View File

@ -22,7 +22,7 @@
</template>
<script setup>
import { ref, computed, watch, onMounted, onUpdated } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useQuery } from '@vue/apollo-composable'
import { useRoute } from 'vue-router'
import { selectCommunities } from '@/graphql/queries'
@ -50,6 +50,9 @@ onResult(({ data }) => {
if (data) {
communities.value = data.communities
setDefaultCommunity()
if (data.communities.length === 1) {
validCommunityIdentifier.value = true
}
}
})

View File

@ -2,24 +2,10 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import ContributionForm from './ContributionForm.vue'
// Mock external components and dependencies
vi.mock('@/components/Inputs/InputAmount', () => ({
default: {
name: 'InputAmount',
template: '<input data-testid="input-amount" />',
},
}))
vi.mock('@/components/Inputs/InputTextarea', () => ({
default: {
name: 'InputTextarea',
template: '<textarea data-testid="input-textarea"></textarea>',
},
}))
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key) => key,
d: (date) => date,
}),
}))

View File

@ -19,6 +19,7 @@
class="mb-4 bg-248"
type="date"
:rules="validationSchema.fields.contributionDate"
:disable-smart-valid-state="disableSmartValidState"
@update:model-value="updateField"
/>
<div v-if="noOpenCreation" class="p-3" data-test="contribution-message">
@ -33,6 +34,7 @@
:placeholder="$t('contribution.yourActivity')"
:rules="validationSchema.fields.memo"
textarea="true"
:disable-smart-valid-state="disableSmartValidState"
@update:model-value="updateField"
/>
<ValidatedInput
@ -41,8 +43,9 @@
:label="$t('form.hours')"
placeholder="0.01"
step="0.01"
type="number"
type="text"
:rules="validationSchema.fields.hours"
:disable-smart-valid-state="disableSmartValidState"
@update:model-value="updateField"
/>
<LabeledInput
@ -68,7 +71,7 @@
{{ $t('form.cancel') }}
</BButton>
</BCol>
<BCol class="text-end mt-lg-0">
<BCol class="text-end mt-lg-0" @mouseover="disableSmartValidState = true">
<BButton
block
type="submit"
@ -89,9 +92,8 @@ import { reactive, computed, ref, onMounted, onUnmounted, toRaw } from 'vue'
import { useI18n } from 'vue-i18n'
import ValidatedInput from '@/components/Inputs/ValidatedInput'
import LabeledInput from '@/components/Inputs/LabeledInput'
import { memo as memoSchema } from '@/validationSchemas'
import OpenCreationsAmount from './OpenCreationsAmount.vue'
import { object, date as dateSchema, number } from 'yup'
import { object, date as dateSchema, number, string } from 'yup'
import { GDD_PER_HOUR } from '../../constants'
const amountToHours = (amount) => parseFloat(amount / GDD_PER_HOUR).toFixed(2)
@ -105,7 +107,7 @@ const props = defineProps({
const emit = defineEmits(['upsert-contribution', 'abort'])
const { t } = useI18n()
const { t, d } = useI18n()
const entityDataToForm = computed(() => ({
...props.modelValue,
@ -121,6 +123,7 @@ const entityDataToForm = computed(() => ({
const form = reactive({ ...entityDataToForm.value })
const now = ref(new Date()) // checked every minute, updated if day, month or year changed
const disableSmartValidState = ref(false)
const isThisMonth = computed(() => {
const formContributionDate = new Date(form.contributionDate)
@ -147,16 +150,26 @@ const validationSchema = computed(() => {
// The date field is required and needs to be a valid date
// contribution date
contributionDate: dateSchema()
.required()
.min(minimalDate.value.toISOString().slice(0, 10)) // min date is first day of last month
.max(now.value.toISOString().slice(0, 10)), // date cannot be in the future
memo: memoSchema,
.required('form.validation.contributionDate.required')
.min(minimalDate.value.toISOString().slice(0, 10), ({ min }) => ({
key: 'form.validation.contributionDate.min',
values: { min: d(min) },
})) // min date is first day of last month
.max(now.value.toISOString().slice(0, 10), ({ max }) => ({
key: 'form.validation.contributionDate.max',
values: { max: d(max) },
})), // date cannot be in the future
memo: string()
.min(5, ({ min }) => ({ key: 'form.validation.contributionMemo.min', values: { min } }))
.max(512, ({ max }) => ({ key: 'form.validation.contributionMemo.max', values: { max } }))
.required('form.validation.contributionMemo.required'),
hours: number()
.typeError({ key: 'form.validation.hours.typeError', values: { min: 0.01, max: maxHours } })
.required()
.transform((value, originalValue) => (originalValue === '' ? undefined : value))
.min(0.01, ({ min }) => ({ key: 'form.validation.gddCreationTime.min', values: { min } }))
.max(maxHours, ({ max }) => ({ key: 'form.validation.gddCreationTime.max', values: { max } }))
.test('decimal-places', 'form.validation.gddCreationTime.decimal-places', (value) => {
// .transform((value, originalValue) => (originalValue === '' ? undefined : value))
.min(0.01, ({ min }) => ({ key: 'form.validation.hours.min', values: { min } }))
.max(maxHours, ({ max }) => ({ key: 'form.validation.hours.max', values: { max } }))
.test('decimal-places', 'form.validation.hours.decimal-places', (value) => {
if (value === undefined || value === null) return true
return /^\d+(\.\d{0,2})?$/.test(value.toString())
}),

View File

@ -22,7 +22,7 @@ const i18n = createI18n({
},
edit: 'Edit',
delete: 'Delete',
moderatorChat: 'Chat',
Chat: 'Chat',
},
},
datetimeFormats: {

View File

@ -22,7 +22,7 @@ const i18n = createI18n({
},
edit: 'Edit',
delete: 'Delete',
moderatorChat: 'Chat',
Chat: 'Chat',
},
},
datetimeFormats: {

View File

@ -25,6 +25,8 @@
@click="emit('toggle-messages-visible')"
>
{{ $t('contribution.alert.answerQuestion') }}
<br />
{{ $t('answerNow') }}
</div>
</BCol>
<BCol cols="9" lg="3" offset="3" offset-md="0" offset-lg="0">
@ -78,7 +80,7 @@
@click="emit('toggle-messages-visible')"
>
<IBiChatDots />
<div>{{ $t('moderatorChat') }}</div>
<div>{{ $t('Chat') }}</div>
</div>
</BCol>
</BRow>

View File

@ -3,8 +3,16 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import TransactionForm from './TransactionForm'
import { nextTick, ref } from 'vue'
import { SEND_TYPES } from '@/utils/sendTypes'
import { BCard, BForm, BFormRadioGroup, BRow, BCol, BFormRadio, BButton } from 'bootstrap-vue-next'
import { useForm } from 'vee-validate'
import {
BCard,
BForm,
BFormRadioGroup,
BRow,
BCol,
BFormRadio,
BButton,
BFormInvalidFeedback,
} from 'bootstrap-vue-next'
import { useRoute } from 'vue-router'
vi.mock('vue-router', () => ({
@ -35,23 +43,6 @@ vi.mock('@/composables/useToast', () => ({
})),
}))
vi.mock('vee-validate', () => {
const actualUseForm = vi.fn().mockReturnValue({
handleSubmit: vi.fn((callback) => {
return () =>
callback({
identifier: 'test@example.com',
amount: '100,00',
memo: 'Test memo',
})
}),
resetForm: vi.fn(),
defineField: vi.fn(() => [vi.fn(), {}]),
})
return { useForm: actualUseForm }
})
describe('TransactionForm', () => {
let wrapper
@ -64,6 +55,9 @@ describe('TransactionForm', () => {
mocks: {
$t: mockT,
$n: mockN,
$i18n: {
locale: 'en',
},
},
components: {
BCard,
@ -73,12 +67,11 @@ describe('TransactionForm', () => {
BCol,
BFormRadio,
BButton,
BFormInvalidFeedback,
},
stubs: {
'community-switch': true,
'input-identifier': true,
'input-amount': true,
'input-textarea': true,
'validated-input': true,
},
},
props: {
@ -102,15 +95,15 @@ describe('TransactionForm', () => {
describe('with balance <= 0.00 GDD the form is disabled', () => {
it('has a disabled input field of type text', () => {
expect(wrapper.find('input-identifier-stub').attributes('disabled')).toBe('true')
expect(wrapper.find('#identifier').attributes('disabled')).toBe('true')
})
it('has a disabled input field for amount', () => {
expect(wrapper.find('input-amount-stub').attributes('disabled')).toBe('true')
expect(wrapper.find('#amount').attributes('disabled')).toBe('true')
})
it('has a disabled textarea field', () => {
expect(wrapper.find('input-textarea-stub').attributes('disabled')).toBe('true')
expect(wrapper.find('#memo').attributes('disabled')).toBe('true')
})
it('has a message indicating that there are no GDDs to send', () => {
@ -143,41 +136,39 @@ describe('TransactionForm', () => {
describe('identifier field', () => {
it('has an input field of type text', () => {
expect(wrapper.find('input-identifier-stub').exists()).toBe(true)
expect(wrapper.find('#identifier').exists()).toBe(true)
})
it('has a label form.recipient', () => {
expect(wrapper.find('input-identifier-stub').attributes('label')).toBe('form.recipient')
expect(wrapper.find('#identifier').attributes('label')).toBe('form.recipient')
})
it('has a placeholder for identifier', () => {
expect(wrapper.find('input-identifier-stub').attributes('placeholder')).toBe(
'form.identifier',
)
expect(wrapper.find('#identifier').attributes('placeholder')).toBe('form.identifier')
})
})
describe('amount field', () => {
it('has an input field of type text', () => {
expect(wrapper.find('input-amount-stub').exists()).toBe(true)
expect(wrapper.find('#amount').exists()).toBe(true)
})
it('has a label form.amount', () => {
expect(wrapper.find('input-amount-stub').attributes('label')).toBe('form.amount')
expect(wrapper.find('#amount').attributes('label')).toBe('form.amount')
})
it('has a placeholder "0.01"', () => {
expect(wrapper.find('input-amount-stub').attributes('placeholder')).toBe('0.01')
expect(wrapper.find('#amount').attributes('placeholder')).toBe('0.01')
})
})
describe('message text box', () => {
it('has a textarea field', () => {
expect(wrapper.find('input-textarea-stub').exists()).toBe(true)
expect(wrapper.find('#memo').exists()).toBe(true)
})
it('has a label form.message', () => {
expect(wrapper.find('input-textarea-stub').attributes('label')).toBe('form.message')
expect(wrapper.find('#memo').attributes('label')).toBe('form.message')
})
})
@ -233,8 +224,10 @@ describe('TransactionForm', () => {
})
it('emits set-transaction event with correct data when form is submitted', async () => {
wrapper.vm.form.identifier = 'test@example.com'
wrapper.vm.form.amount = '100,00'
wrapper.vm.form.memo = 'Test memo'
await wrapper.findComponent(BForm).trigger('submit.prevent')
expect(wrapper.emitted('set-transaction')).toBeTruthy()
expect(wrapper.emitted('set-transaction')[0][0]).toEqual(
expect.objectContaining({
@ -247,20 +240,10 @@ describe('TransactionForm', () => {
})
it('handles form submission with empty amount', async () => {
vi.mocked(useForm).mockReturnValueOnce({
...vi.mocked(useForm)(),
handleSubmit: vi.fn((callback) => {
return () =>
callback({
identifier: 'test@example.com',
amount: '',
memo: 'Test memo',
})
}),
})
wrapper = createWrapper({ balance: 100.0 })
await nextTick()
wrapper.vm.form.identifier = 'test@example.com'
wrapper.vm.form.memo = 'Test memo'
await wrapper.findComponent(BForm).trigger('submit.prevent')
expect(wrapper.emitted('set-transaction')).toBeTruthy()

View File

@ -46,20 +46,25 @@
<BRow>
<BCol class="fw-bold">
<community-switch
:disabled="isBalanceDisabled"
:model-value="targetCommunity"
@update:model-value="targetCommunity = $event"
:disabled="isBalanceEmpty"
:model-value="form.targetCommunity"
@update:model-value="updateField($event, 'targetCommunity')"
/>
</BCol>
</BRow>
</BCol>
<BCol v-if="radioSelected === SEND_TYPES.send" cols="12">
<div v-if="!userIdentifier">
<input-identifier
<ValidatedInput
id="identifier"
:model-value="form.identifier"
name="identifier"
:label="$t('form.recipient')"
:placeholder="$t('form.identifier')"
:disabled="isBalanceDisabled"
:rules="validationSchema.fields.identifier"
:disabled="isBalanceEmpty"
:disable-smart-valid-state="disableSmartValidState"
@update:model-value="updateField"
/>
</div>
<div v-else class="mb-4">
@ -72,13 +77,17 @@
</div>
</BCol>
<BCol cols="12" lg="6">
<input-amount
<ValidatedInput
id="amount"
:model-value="form.amount"
name="amount"
:label="$t('form.amount')"
:placeholder="'0.01'"
:rules="{ required: true, gddSendAmount: { min: 0.01, max: balance } }"
:disabled="isBalanceDisabled"
></input-amount>
:rules="validationSchema.fields.amount"
:disabled="isBalanceEmpty"
:disable-smart-valid-state="disableSmartValidState"
@update:model-value="updateField"
/>
</BCol>
</BRow>
</BCol>
@ -86,16 +95,21 @@
<BRow>
<BCol>
<input-textarea
<ValidatedInput
id="memo"
:model-value="form.memo"
name="memo"
:label="$t('form.message')"
:placeholder="$t('form.message')"
:rules="{ required: true, min: 5, max: 255 }"
:disabled="isBalanceDisabled"
:rules="validationSchema.fields.memo"
textarea="true"
:disabled="isBalanceEmpty"
:disable-smart-valid-state="disableSmartValidState"
@update:model-value="updateField"
/>
</BCol>
</BRow>
<div v-if="!!isBalanceDisabled" class="text-danger mt-5">
<div v-if="!!isBalanceEmpty" class="text-danger mt-5">
{{ $t('form.no_gdd_available') }}
</div>
<BRow v-else class="test-buttons mt-3">
@ -110,8 +124,14 @@
{{ $t('form.reset') }}
</BButton>
</BCol>
<BCol cols="12" md="6" lg="6" class="text-lg-end">
<BButton block type="submit" variant="gradido">
<BCol
cols="12"
md="6"
lg="6"
class="text-lg-end"
@mouseover="disableSmartValidState = true"
>
<BButton block type="submit" variant="gradido" :disabled="formIsInvalid">
{{ $t('form.check_now') }}
</BButton>
</BCol>
@ -124,15 +144,14 @@
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, reactive } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useQuery } from '@vue/apollo-composable'
import { useForm } from 'vee-validate'
import { SEND_TYPES } from '@/utils/sendTypes'
import InputIdentifier from '@/components/Inputs/InputIdentifier'
import InputAmount from '@/components/Inputs/InputAmount'
import InputTextarea from '@/components/Inputs/InputTextarea'
import CommunitySwitch from '@/components/CommunitySwitch.vue'
import ValidatedInput from '@/components/Inputs/ValidatedInput.vue'
import { memo as memoSchema, identifier as identifierSchema } from '@/validationSchemas'
import { object, number } from 'yup'
import { user } from '@/graphql/queries'
import CONFIG from '@/config'
import { useAppToast } from '@/composables/useToast'
@ -149,6 +168,10 @@ const props = defineProps({
},
})
const entityDataToForm = computed(() => ({ ...props }))
const form = reactive({ ...entityDataToForm.value })
const disableSmartValidState = ref(false)
const emit = defineEmits(['set-transaction'])
const route = useRoute()
@ -157,18 +180,6 @@ const { toastError } = useAppToast()
const radioSelected = ref(props.selected)
const userName = ref('')
const recipientCommunity = ref({ uuid: '', name: '' })
const { handleSubmit, resetForm, defineField, values } = useForm({
initialValues: {
identifier: props.identifier,
amount: props.amount ? String(props.amount) : '',
memo: props.memo,
targetCommunity: props.targetCommunity,
},
})
const [targetCommunity, targetCommunityProps] = defineField('targetCommunity')
const userIdentifier = computed(() => {
if (route.params.userIdentifier && route.params.communityIdentifier) {
@ -180,7 +191,49 @@ const userIdentifier = computed(() => {
return null
})
const isBalanceDisabled = computed(() => props.balance <= 0)
const validationSchema = computed(() => {
const amountSchema = number()
.required()
.typeError({
key: 'form.validation.amount.typeError',
values: { min: 0.01, max: props.balance },
})
.transform((value, originalValue) => {
if (typeof originalValue === 'string') {
return Number(originalValue.replace(',', '.'))
}
return value
})
.min(0.01, ({ min }) => ({ key: 'form.validation.amount.min', values: { min } }))
.max(props.balance, ({ max }) => ({ key: 'form.validation.amount.max', values: { max } }))
.test('decimal-places', 'form.validation.amount.decimal-places', (value) => {
if (value === undefined || value === null) return true
return /^\d+(\.\d{0,2})?$/.test(value.toString())
})
if (!userIdentifier.value && radioSelected.value === SEND_TYPES.send) {
return object({
memo: memoSchema,
amount: amountSchema,
identifier: identifierSchema,
})
} else {
// don't need identifier schema if it is a transaction link or identifier was set via url
return object({
memo: memoSchema,
amount: amountSchema,
})
}
})
const formIsInvalid = computed(() => !validationSchema.value.isValidSync(form))
const updateField = (newValue, name) => {
if (typeof name === 'string' && name.length) {
form[name] = newValue
}
}
const isBalanceEmpty = computed(() => props.balance <= 0)
const { result: userResult, error: userError } = useQuery(
user,
@ -193,6 +246,7 @@ watch(
(user) => {
if (user) {
userName.value = `${user.firstName} ${user.lastName}`
form.identifier = userIdentifier.value.identifier
}
},
{ immediate: true },
@ -204,19 +258,21 @@ watch(userError, (error) => {
}
})
const onSubmit = handleSubmit((formValues) => {
if (userIdentifier.value) formValues.identifier = userIdentifier.value.identifier
function onSubmit() {
const transformedForm = validationSchema.value.cast(form)
emit('set-transaction', {
...transformedForm,
selected: radioSelected.value,
...formValues,
amount: Number(formValues.amount.replace(',', '.')),
userName: userName.value,
})
})
}
function onReset(event) {
event.preventDefault()
resetForm()
form.amount = props.amount
form.memo = props.memo
form.identifier = props.identifier
form.targetCommunity = props.targetCommunity
radioSelected.value = SEND_TYPES.send
router.replace('/send')
}

View File

@ -1,125 +0,0 @@
import { mount } from '@vue/test-utils'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import InputAmount from './InputAmount'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ref } from 'vue'
import { BFormInput } from 'bootstrap-vue-next'
vi.mock('vue-router', () => ({
useRoute: vi.fn(() => ({
params: {},
path: '/some-path',
})),
}))
vi.mock('vue-i18n', () => ({
useI18n: vi.fn(() => ({
t: (key) => key,
n: (num) => num,
})),
}))
vi.mock('vee-validate', () => ({
useField: vi.fn(() => ({
value: ref(''),
meta: { valid: true },
errorMessage: ref(''),
})),
}))
// Mock toast
const mockToastError = vi.fn()
vi.mock('@/composables/useToast', () => ({
useAppToast: vi.fn(() => ({
toastError: mockToastError,
})),
}))
describe('InputAmount', () => {
let wrapper
const createWrapper = (propsData = {}) => {
return mount(InputAmount, {
props: {
name: 'amount',
label: 'Amount',
placeholder: 'Enter amount',
typ: 'TransactionForm',
modelValue: '12,34',
...propsData,
},
global: {
mocks: {
$route: useRoute(),
...useI18n(),
},
components: {
BFormInput,
},
directives: {
focus: {},
},
stubs: {
BFormGroup: true,
BFormInvalidFeedback: true,
BInputGroup: true,
},
},
})
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('mount in a TransactionForm', () => {
beforeEach(() => {
wrapper = createWrapper()
})
it('renders the component input-amount', () => {
expect(wrapper.find('div.input-amount').exists()).toBe(true)
})
it('normalizes the amount correctly', async () => {
await wrapper.vm.normalizeAmount('12,34')
expect(wrapper.vm.value).toBe('12.34')
})
it('does not normalize invalid input', async () => {
await wrapper.vm.normalizeAmount('12m34')
expect(wrapper.vm.value).toBe('12m34')
})
})
describe('mount in a ContributionForm', () => {
beforeEach(() => {
wrapper = createWrapper({
typ: 'ContributionForm',
modelValue: '12.34',
})
})
it('renders the component input-amount', () => {
expect(wrapper.find('div.input-amount').exists()).toBe(true)
})
it('normalizes the amount correctly', async () => {
await wrapper.vm.normalizeAmount('12.34')
expect(wrapper.vm.value).toBe('12.34')
})
it('does not normalize invalid input', async () => {
await wrapper.vm.normalizeAmount('12m34')
expect(wrapper.vm.value).toBe('12m34')
})
})
it('emits update:modelValue when value changes', async () => {
wrapper = createWrapper()
await wrapper.vm.normalizeAmount('15.67')
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['15.67'])
})
})

View File

@ -1,88 +0,0 @@
<template>
<div class="input-amount">
<template v-if="typ === 'TransactionForm'">
<BFormGroup :label="label" :label-for="labelFor" data-test="input-amount">
<BFormInput
:id="labelFor"
v-focus="amountFocused"
:model-value="value"
:class="$route.path === '/send' ? 'bg-248' : ''"
:name="name"
:placeholder="placeholder"
type="text"
:state="meta.valid"
trim
:disabled="disabled"
autocomplete="off"
@update:model-value="normalizeAmount($event)"
@focus="amountFocused = true"
@blur="normalizeAmount($event)"
/>
<BFormInvalidFeedback v-if="errorMessage">
{{ errorMessage }}
</BFormInvalidFeedback>
</BFormGroup>
</template>
<BInputGroup v-else append="GDD" :label="label" :label-for="labelFor">
<BFormInput
:id="labelFor"
v-focus="amountFocused"
:model-value="value"
:name="name"
:placeholder="placeholder"
type="text"
readonly
trim
/>
</BInputGroup>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useField } from 'vee-validate'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
const props = defineProps({
rules: {
type: Object,
default: () => ({}),
},
typ: { type: String, default: 'TransactionForm' },
name: { type: String, required: true, default: 'Amount' },
label: { type: String, required: true, default: 'Amount' },
placeholder: { type: String, required: true, default: 'Amount' },
balance: { type: Number, default: 0.0 },
disabled: { required: false, type: Boolean, default: false },
})
const emit = defineEmits(['update:modelValue'])
const route = useRoute()
const { n } = useI18n()
const { value, meta, errorMessage } = useField(props.name, props.rules)
const amountFocused = ref(false)
const amountValue = ref(0.0)
const labelFor = computed(() => props.name + '-input-field')
watch(value, (newValue) => {
emit('update:modelValue', newValue)
})
watch(
() => props.modelValue,
(newValue) => {
if (newValue !== value.value) value.value = newValue
},
)
const normalizeAmount = (inputValue) => {
amountFocused.value = false
if (typeof inputValue === 'string' && inputValue.length > 1) {
value.value = inputValue.replace(',', '.')
}
}
</script>

View File

@ -1,61 +0,0 @@
<template>
<BFormGroup :label="label" :label-for="labelFor" data-test="input-identifier">
<BFormInput
:id="labelFor"
:model-value="value"
:name="name"
:placeholder="placeholder"
type="text"
:state="meta.valid"
trim
class="bg-248"
:disabled="disabled"
autocomplete="off"
@update:model-value="value = $event"
/>
<BFormInvalidFeedback v-if="errorMessage">
{{ errorMessage }}
</BFormInvalidFeedback>
</BFormGroup>
</template>
<script setup>
import { computed, watch } from 'vue'
import { useField } from 'vee-validate'
const props = defineProps({
rules: {
type: Object,
default: () => ({
required: true,
validIdentifier: true,
}),
},
name: { type: String, required: true },
label: { type: String, required: true },
placeholder: { type: String, required: true },
modelValue: { type: String },
disabled: { type: Boolean, required: false, default: false },
})
const emit = defineEmits(['update:modelValue', 'onValidation'])
const { value, meta, errorMessage } = useField(props.name, props.rules, {
initialValue: props.modelValue,
})
const labelFor = computed(() => props.name + '-input-field')
watch(value, (newValue) => {
emit('update:modelValue', newValue)
})
watch(
() => props.modelValue,
(newValue) => {
if (newValue !== value.value) {
value.value = newValue
emit('onValidation')
}
},
)
</script>

View File

@ -1,125 +0,0 @@
import { mount } from '@vue/test-utils'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import InputTextarea from './InputTextarea'
import { useField } from 'vee-validate'
import { BFormGroup, BFormInvalidFeedback, BFormTextarea } from 'bootstrap-vue-next'
vi.mock('vee-validate', () => ({
useField: vi.fn(),
}))
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key) => key,
}),
}))
describe('InputTextarea', () => {
let wrapper
const createWrapper = (props = {}) => {
return mount(InputTextarea, {
props: {
rules: {},
name: 'input-field-name',
label: 'input-field-label',
placeholder: 'input-field-placeholder',
...props,
},
global: {
components: {
BFormGroup,
BFormTextarea,
BFormInvalidFeedback,
},
},
})
}
beforeEach(() => {
vi.mocked(useField).mockReturnValue({
value: '',
errorMessage: '',
meta: { valid: true },
})
wrapper = createWrapper()
})
it('renders the component InputTextarea', () => {
expect(wrapper.find('[data-test="input-textarea"]').exists()).toBe(true)
})
it('has a textarea field', () => {
expect(wrapper.findComponent({ name: 'BFormTextarea' }).exists()).toBe(true)
})
describe('properties', () => {
it('has the correct id', () => {
const textarea = wrapper.findComponent({ name: 'BFormTextarea' })
expect(textarea.attributes('id')).toBe('input-field-name-input-field')
})
it('has the correct placeholder', () => {
const textarea = wrapper.findComponent({ name: 'BFormTextarea' })
expect(textarea.attributes('placeholder')).toBe('input-field-placeholder')
})
it('has the correct label', () => {
const label = wrapper.find('label')
expect(label.text()).toBe('input-field-label')
})
it('has the correct label-for attribute', () => {
const label = wrapper.find('label')
expect(label.attributes('for')).toBe('input-field-name-input-field')
})
})
describe('input value changes', () => {
it('updates the model value when input changes', async () => {
const wrapper = mount(InputTextarea, {
props: {
rules: {},
name: 'input-field-name',
label: 'input-field-label',
placeholder: 'input-field-placeholder',
},
global: {
components: {
BFormGroup,
BFormInvalidFeedback,
BFormTextarea,
},
},
})
const textarea = wrapper.find('textarea')
await textarea.setValue('New Text')
expect(wrapper.vm.currentValue).toBe('New Text')
})
})
describe('disabled state', () => {
it('disables the textarea when disabled prop is true', async () => {
await wrapper.setProps({ disabled: true })
const textarea = wrapper.findComponent({ name: 'BFormTextarea' })
expect(textarea.attributes('disabled')).toBeDefined()
})
})
it('shows error message when there is an error', async () => {
vi.mocked(useField).mockReturnValue({
value: '',
errorMessage: 'This field is required',
meta: { valid: false },
})
wrapper = createWrapper()
await wrapper.vm.$nextTick()
const errorFeedback = wrapper.findComponent({ name: 'BFormInvalidFeedback' })
expect(errorFeedback.exists()).toBe(true)
expect(errorFeedback.text()).toBe('This field is required')
})
})

View File

@ -1,64 +0,0 @@
<template>
<div>
<BFormGroup :label="label" :label-for="labelFor" data-test="input-textarea">
<BFormTextarea
:id="labelFor"
:model-value="currentValue"
class="bg-248"
:name="name"
:placeholder="placeholder"
:state="meta.valid"
trim
:rows="4"
:max-rows="4"
:disabled="disabled"
no-resize
@update:modelValue="currentValue = $event"
/>
<BFormInvalidFeedback v-if="errorMessage">
{{ translatedErrorString }}
</BFormInvalidFeedback>
</BFormGroup>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useField } from 'vee-validate'
import { useI18n } from 'vue-i18n'
import { translateYupErrorString } from '@/validationSchemas'
const props = defineProps({
rules: {
type: Object,
default: () => ({}),
},
name: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
placeholder: {
type: String,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
})
const { value: currentValue, errorMessage, meta } = useField(props.name, props.rules)
const { t } = useI18n()
const translatedErrorString = computed(() => translateYupErrorString(errorMessage, t))
const labelFor = computed(() => `${props.name}-input-field`)
</script>
<style lang="scss" scoped>
:deep(.form-control) {
height: unset;
}
</style>

View File

@ -0,0 +1,92 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import ValidatedInput from '@/components/Inputs/ValidatedInput.vue'
import * as yup from 'yup'
import { BFormInvalidFeedback, BFormInput, BFormTextarea, BFormGroup } from 'bootstrap-vue-next'
import LabeledInput from '@/components/Inputs/LabeledInput.vue'
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key) => key,
n: (n) => String(n),
}),
}))
describe('ValidatedInput', () => {
let wrapper
const createWrapper = (props = {}) =>
mount(ValidatedInput, {
props: {
label: 'Test Label',
modelValue: '',
name: 'testInput',
rules: yup.string().required().min(3).default(''),
...props,
},
global: {
mocks: {
$t: (key) => key,
$i18n: {
locale: 'en',
},
$n: (n) => String(n),
},
components: {
BFormInvalidFeedback,
BFormInput,
BFormTextarea,
BFormGroup,
LabeledInput,
},
},
})
beforeEach(() => {
wrapper = createWrapper()
})
it('renders the label and input', () => {
expect(wrapper.text()).toContain('Test Label')
const input = wrapper.find('input')
expect(input.exists()).toBe(true)
})
it('starts with neutral validation state', () => {
const input = wrapper.find('input')
expect(input.classes()).not.toContain('is-valid')
expect(input.classes()).not.toContain('is-invalid')
})
it('shows green border when value is valid before blur', async () => {
await wrapper.setProps({ modelValue: 'validInput' })
await wrapper.vm.$nextTick()
const input = wrapper.find('input')
expect(input.classes()).toContain('is-valid')
expect(input.classes()).not.toContain('is-invalid')
})
it('does not show red border before blur even if invalid', async () => {
await wrapper.setProps({ modelValue: 'a' })
const input = wrapper.find('input')
expect(input.classes()).not.toContain('is-invalid')
})
it('shows red border and error message after blur when input is invalid', async () => {
await wrapper.setProps({ modelValue: 'a' })
const input = wrapper.find('input')
await input.trigger('blur')
await wrapper.vm.$nextTick()
expect(input.classes()).toContain('is-invalid')
expect(wrapper.text()).toContain('this must be at least 3 characters')
})
it('emits update:modelValue on input', async () => {
const input = wrapper.find('input')
await input.setValue('hello')
await wrapper.vm.$nextTick()
expect(wrapper.emitted()['update:modelValue']).toBeTruthy()
const [value, name] = wrapper.emitted()['update:modelValue'][0]
expect(value).toBe('hello')
expect(name).toBe('testInput')
})
})

View File

@ -9,7 +9,8 @@
:required="!isOptional"
:label="label"
:name="name"
:state="valid"
:state="smartValidState"
@blur="afterFirstInput = true"
@update:modelValue="updateValue"
>
<BFormInvalidFeedback v-if="errorMessage">
@ -19,7 +20,7 @@
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { computed, ref, watch, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import LabeledInput from './LabeledInput'
import { translateYupErrorString } from '@/validationSchemas'
@ -38,19 +39,40 @@ const props = defineProps({
type: Object,
required: true,
},
disableSmartValidState: {
type: Boolean,
default: false,
},
})
const { t } = useI18n()
const model = ref(props.modelValue)
const model = ref(props.modelValue !== 0 ? props.modelValue : '')
// change to true after user leave the input field the first time
// prevent showing errors on form init
const afterFirstInput = ref(false)
const valid = computed(() => props.rules.isValidSync(props.modelValue))
const errorMessage = computed(() => {
if (props.modelValue === undefined || props.modelValue === '' || props.modelValue === null) {
return undefined
const valid = computed(() => props.rules.isValidSync(model.value))
// smartValidState controls the visual validation feedback for the input field.
// The goal is to avoid showing red (invalid) borders too early, creating a smoother UX:
//
// - On initial form open, the field is neutral (no validation state shown).
// - If the user enters a value that passes validation, we show a green (valid) state immediately.
// - We only show red (invalid) feedback *after* the user has blurred the field for the first time.
//
// Before first blur:
// - show green if valid, otherwise neutral (null)
// After first blur:
// - show true or false according to the validation result
const smartValidState = computed(() => {
if (afterFirstInput.value || props.disableSmartValidState) {
return valid.value
}
return valid.value ? true : null
})
const errorMessage = computed(() => {
try {
props.rules.validateSync(props.modelValue)
props.rules.validateSync(model.value)
return undefined
} catch (e) {
return translateYupErrorString(e.message, t)
@ -79,4 +101,17 @@ const minValue = computed(() => getTestParameter('min'))
const maxValue = computed(() => getTestParameter('max'))
const resetValue = computed(() => schemaDescription.value.default)
const isOptional = computed(() => schemaDescription.value.optional)
// reset on mount
onMounted(() => {
afterFirstInput.value = false
})
</script>
<!-- disable animation on invalid input -->
<style>
.form-control {
transition: none !important;
transform: none !important;
animation: none !important;
}
</style>

View File

@ -29,13 +29,13 @@ const i18n = createI18n({
overview: 'Overview',
send: 'Send',
transactions: 'Transactions',
info: 'Info',
circles: 'Circles',
usersearch: 'User Search',
settings: 'Settings',
admin_area: 'Admin Area',
logout: 'Logout',
},
info: 'Info',
creation: 'Creation',
},
},
@ -108,7 +108,7 @@ describe('Sidebar', () => {
expect(wrapper.findAll('.nav-item').at(3).text()).toContain('Creation')
})
it('has nav-item "navigation.info" in navbar', () => {
it('has nav-item "info" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(4).text()).toContain('Info')
})
})

View File

@ -40,7 +40,7 @@
<BNavItem to="/information" class="mb-3" active-class="active-route">
<div class="sidebar-menu-item-wrapper">
<i-mdi-information class="svg-icon" />
<span class="ms-2">{{ $t('navigation.info') }}</span>
<span class="ms-2">{{ $t('info') }}</span>
</div>
</BNavItem>
</BNav>

View File

@ -21,7 +21,7 @@
</span>
</div>
<a href="https://gradido.net/gms1/" target="_blank">
{{ $t('info') }}
{{ $t('card-user-search.info') }}
</a>
<BRow class="my-1">
<BCol cols="12">

View File

@ -20,7 +20,7 @@ export function useAppToast() {
const toastInfo = (message) => {
toast(message, {
title: t('navigation.info'),
title: t('info'),
variant: 'warning',
bodyClass: 'gdd-toaster-body-darken',
})

View File

@ -40,11 +40,13 @@
<BRow>
<BCol cols="12" lg="5">
<div>
<gdd-amount
:balance="balance"
:show-status="false"
:badge-show="false"
/>
<router-link to="transactions">
<gdd-amount
:balance="balance"
:show-status="false"
:badge-show="false"
/>
</router-link>
</div>
</BCol>
<BCol cols="12" lg="7">
@ -79,9 +81,7 @@
<BRow>
<BCol cols="12" lg="6">
<div>
<router-link to="transactions">
<gdd-amount :balance="balance" :show-status="true" />
</router-link>
<gdd-amount :balance="balance" :show-status="true" />
</div>
</BCol>
<BCol cols="12" lg="6">
@ -104,13 +104,11 @@
</BCol>
<BCol cols="12" lg="6">
<div>
<router-link to="gdt">
<gdt-amount
:badge="true"
:show-status="true"
:gdt-balance="GdtBalance"
/>
</router-link>
<gdt-amount
:badge="true"
:show-status="true"
:gdt-balance="GdtBalance"
/>
</div>
</BCol>
</BRow>

View File

@ -5,6 +5,7 @@
"1000thanks": "1000 Dank, weil du bei uns bist!",
"125": "125%",
"85": "85%",
"Chat": "Chat",
"GDD": "GDD",
"GDT": "GDT",
"GMS": {
@ -17,6 +18,7 @@
},
"PersonalDetails": "Persönliche Angaben",
"advanced-calculation": "Vorausberechnung",
"answerNow": "→ Jetzt antworten!",
"asterisks": "****",
"auth": {
"left": {
@ -34,7 +36,7 @@
"headline": "Kooperationsplattform »Gradido-Kreise«",
"text": "Lokale Kreise, Studienkreise, Projekte, Events und Kongresse",
"allowed": {
"button": "Kreise starten..."
"button": "Kreise öffnen..."
},
"not-allowed": {
"button": "Konfigurieren..."
@ -43,14 +45,15 @@
"card-user-search": {
"headline": "Geografische Mitgliedssuche (beta)",
"allowed": {
"button": "Öffne Mitgliedssuche...",
"button": "Mitgliedssuche öffnen...",
"disabled-button": "GMS offline...",
"text": "Finde Mitglieder aller Communities auf einer Landkarte."
},
"not-allowed": {
"button": "Standort festlegen...",
"text": "Finde Mitglieder aller Communities auf einer Landkarte? Dann musst du selbst erst deinen Standort festlegen."
}
"button": "Standort eintragen...",
"text": "Um andere Mitglieder in deinem Umkreis zu finden, trage jetzt deinen Standort auf der Karte ein!"
},
"info": "So gehts"
},
"community": {
"admins": "Administratoren",
@ -203,22 +206,43 @@
"username": "Benutzername",
"username-placeholder": "Wähle deinen Benutzernamen",
"validation": {
"gddCreationTime": {
"min": "Die Stunden sollten mindestens {min} groß sein",
"max": "Die Stunden sollten höchstens {max} groß sein",
"decimal-places": "Die Stunden sollten maximal zwei Nachkommastellen enthalten"
"amount": {
"min": "Der Betrag sollte mindestens {min} groß sein.",
"max": "Der Betrag sollte höchstens {max} groß sein.",
"decimal-places": "Der Betrag sollte maximal zwei Nachkommastellen enthalten.",
"typeError": "Der Betrag sollte eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein."
},
"gddSendAmount": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein",
"is-not": "Du kannst dir selbst keine Gradidos überweisen",
"contributionDate": {
"required": "Das Beitragsdatum ist ein Pflichtfeld.",
"min": "Das Frühste Beitragsdatum ist {min}.",
"max": "Das Späteste Beitragsdatum ist heute, der {max}."
},
"contributionMemo": {
"min": "Die Tätigkeitsbeschreibung sollte mindestens {min} Zeichen lang sein.",
"max": "Die Tätigkeitsbeschreibung sollte höchstens {max} Zeichen lang sein.",
"required": "Die Tätigkeitsbeschreibung ist ein Pflichtfeld."
},
"hours": {
"min": "Die Stunden sollten mindestens {min} groß sein.",
"max": "Die Stunden sollten höchstens {max} groß sein.",
"decimal-places": "Die Stunden sollten maximal zwei Nachkommastellen enthalten.",
"typeError": "Die Stunden sollten eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein."
},
"gddSendAmount": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein.",
"identifier": {
"required": "Der Empfänger ist ein Pflichtfeld.",
"typeError": "Der Empfänger muss eine Email, ein Nutzernamen oder eine Gradido ID sein."
},
"is-not": "Du kannst dir selbst keine Gradidos überweisen!",
"memo": {
"min": "Die Tätigkeitsbeschreibung sollte mindestens {min} Zeichen lang sein",
"max": "Die Tätigkeitsbeschreibung sollte höchstens {max} Zeichen lang sein"
"min": "Die Nachricht sollte mindestens {min} Zeichen lang sein.",
"max": "Die Nachricht sollte höchstens {max} Zeichen lang sein.",
"required": "Die Nachricht ist ein Pflichtfeld."
},
"requiredField": "{fieldName} ist ein Pflichtfeld",
"username-allowed-chars": "Der Nutzername darf nur aus Buchstaben (ohne Umlaute), Zahlen, Binde- oder Unterstrichen bestehen.",
"username-hyphens": "Binde- oder Unterstriche müssen zwischen Buchstaben oder Zahlen stehen.",
"username-unique": "Der Nutzername ist bereits vergeben.",
"valid-identifier": "Muss eine Email, ein Nutzernamen oder eine gradido ID sein."
"username-unique": "Der Nutzername ist bereits vergeben."
},
"your_amount": "Dein Betrag"
},
@ -277,7 +301,7 @@
"recruited-member": "Eingeladenes Mitglied"
},
"h": "h",
"info": "Info",
"info": "Information",
"language": "Sprache",
"link-load": "den letzten Link nachladen | die letzten {n} Links nachladen",
"link-load-more": "weitere {n} Links nachladen",
@ -302,11 +326,9 @@
},
"missingGradidoAccount": "Noch kein {communityName} Konto?",
"moderatorChangedMemo": "Text vom Moderator bearbeitet",
"moderatorChat": "Moderator Chat",
"navigation": {
"admin_area": "Adminbereich",
"community": "Gemeinschaft",
"info": "Information",
"logout": "Abmelden",
"overview": "Übersicht",
"send": "Senden",

View File

@ -5,6 +5,7 @@
"1000thanks": "1000 thanks for being with us!",
"125": "125%",
"85": "85%",
"Chat": "Chat",
"GDD": "GDD",
"GDT": "GDT",
"GMS": {
@ -17,6 +18,7 @@
},
"PersonalDetails": "Personal details",
"advanced-calculation": "Advanced calculation",
"answerNow": "→ Reply now!",
"asterisks": "****",
"auth": {
"left": {
@ -34,7 +36,7 @@
"headline": "Cooperation platform “Gradido Circles”",
"text": "Local circles, study circles, projects, events and congresses",
"allowed": {
"button": "Start Circles..."
"button": "Open Circles..."
},
"not-allowed": {
"button": "Configurate..."
@ -43,14 +45,15 @@
"card-user-search": {
"headline": "Geographic member search (beta)",
"allowed": {
"button": "Start Membersearch...",
"button": "Open member search...",
"disabled-button": "GMS offline...",
"text": "Find Members of all Communities on a Map."
},
"not-allowed": {
"button": "Start Location-Capturing...",
"text": "Find Members of all Communities on a Map? Then you have to capture your Location first."
}
"button": "Enter location...",
"text": "To find other members in your area, enter your location on the map now!"
},
"info": "How it works"
},
"community": {
"admins": "Administrators",
@ -68,7 +71,7 @@
"contribution": {
"activity": "Activity",
"alert": {
"answerQuestion": "There is a new message for this article.",
"answerQuestion": "There is a new message for this contribution.",
"answerQuestionToast": "You have new messages.",
"communityNoteList": "Here you will find all submitted and confirmed contributions from all members of this community.",
"confirm": "confirmed",
@ -203,22 +206,43 @@
"username": "Username",
"username-placeholder": "Choose your username",
"validation": {
"gddCreationTime": {
"min": "The hours should be at least {min} in size",
"max": "The hours should not be larger than {max}",
"decimal-places": "The hours should contain a maximum of two decimal places"
"amount": {
"min": "The amount should be at least {min} in size.",
"max": "The amount should not be larger than {max}.",
"decimal-places": "The amount should contain a maximum of two decimal places.",
"typeError": "The amount should be a number between {min} and {max} with at most two digits after the decimal point."
},
"gddSendAmount": "The {_field_} field must be a number between {min} and {max} with at most two digits after the decimal point",
"is-not": "You cannot send Gradidos to yourself",
"contributionDate": {
"required": "The contribution date is a required field.",
"min": "The earliest contribution date is {min}.",
"max": "The latest contribution date is today, {max}."
},
"contributionMemo": {
"min": "The job description should be at least {min} characters long.",
"max": "The job description should not be longer than {max} characters.",
"required": "The job description is required."
},
"hours": {
"min": "The hours should be at least {min} in size.",
"max": "The hours should not be larger than {max}.",
"decimal-places": "The hours should contain a maximum of two decimal places.",
"typeError": "The hours should be a number between {min} and {max} with at most two digits after the decimal point."
},
"gddSendAmount": "The {_field_} field must be a number between {min} and {max} with at most two digits after the decimal point.",
"identifier": {
"required": "The recipient is a required field.",
"typeError": "The recipient must be an email, a username or a Gradido ID."
},
"is-not": "You cannot send Gradidos to yourself!",
"memo": {
"min": "The job description should be at least {min} characters long",
"max": "The job description should not be longer than {max} characters"
"min": "The message should be at least {min} characters long.",
"max": "The message should not be longer than {max} characters.",
"required": "The message is required."
},
"requiredField": "The {fieldName} field is required",
"username-allowed-chars": "The username may only contain letters, numbers, hyphens or underscores.",
"username-hyphens": "Hyphens or underscores must be in between letters or numbers.",
"username-unique": "This username is already taken.",
"valid-identifier": "Must be a valid email, username or gradido ID."
"username-unique": "This username is already taken."
},
"your_amount": "Your amount"
},
@ -277,7 +301,7 @@
"recruited-member": "Invited member"
},
"h": "h",
"info": "Info",
"info": "Information",
"language": "Language",
"link-load": "Load the last link | Load the last {n} links",
"link-load-more": "Load more {n} links",
@ -302,11 +326,9 @@
},
"missingGradidoAccount": "Don't have a {communityName} account yet?",
"moderatorChangedMemo": "Text edited by moderator",
"moderatorChat": "Moderator Chat",
"navigation": {
"admin_area": "Admin Area",
"community": "Community",
"info": "Information",
"logout": "Logout",
"overview": "Overview",
"send": "Send",

View File

@ -7,6 +7,7 @@
"GDT": "GDT",
"PersonalDetails": "Datos personales",
"advanced-calculation": "Proyección",
"answerNow": "→ ¡Responde ahora!",
"asterisks": "****",
"auth": {
"left": {
@ -24,7 +25,7 @@
"headline": "Plataforma de cooperación «Círculos Gradido»",
"text": "Círculos locales, círculos de estudio, proyectos, ev entos y congresos",
"allowed": {
"button": "Iniciar círculos..."
"button": "Abrir círculos..."
},
"not-allowed": {
"button": "Configurar..."
@ -33,19 +34,20 @@
"card-user-search": {
"headline": "Búsqueda geográfica de miembros (beta)",
"allowed": {
"button": "Iniciar Búsqueda de Miembros...",
"button": "Abrir búsqueda de miembros...",
"disabled-button": "GMS offline...",
"text": "Encuentra Miembros de todas las Comunidades en un Mapa."
},
"not-allowed": {
"button": "Configuración de ubicación...",
"text": "Encuentra Miembros de todas las Comunidades en un Mapa? Entonces tienes que establecer tu ubicación primero."
}
"button": "Introducir ubicación...",
"text": "Para encontrar otros miembros cerca de ti, ¡introduce ahora tu ubicación en el mapa!"
},
"info": "Así se hace"
},
"circles": {
"headline": "Juntos nos apoyamos - atentos a la cultura de los círculos.",
"text": "Haga clic en el botón para abrir la plataforma de cooperación en una nueva ventana del navegador.",
"button": "Iniciar Círculos..."
"button": "Abrir Círculos..."
},
"community": {
"admins": "Administradores",
@ -69,7 +71,8 @@
"contribution": {
"activity": "Actividad",
"alert": {
"answerQuestion": "Por favor, contesta las preguntas",
"answerQuestion": "Hay una nueva noticia sobre esta contribución.",
"answerQuestionToast": "Tienes mensajes nuevos.",
"communityNoteList": "Aquí encontrarás todas las contribuciones enviadas y confirmadas de todos los miembros de esta comunidad.",
"confirm": "confirmado",
"denied": "rechazado",
@ -239,6 +242,7 @@
"raise": "Aumento",
"recruited-member": "Miembro invitado"
},
"info": "Información",
"language": "Idioma",
"link-load": "recargar el último enlace | recargar los últimos {n} enlaces",
"link-load-more": "descargar más {n} enlaces",
@ -265,7 +269,6 @@
"navigation": {
"admin_area": "Área de administración",
"community": "Comunidad",
"info": "Información",
"logout": "Salir",
"members_area": "Área de afiliados",
"overview": "Resumen",

View File

@ -9,6 +9,7 @@
"GDT": "GDT",
"PersonalDetails": "Informations personnelles",
"advanced-calculation": "Calcul avancé",
"answerNow": "→ Répondre maintenant!",
"asterisks": "****",
"auth": {
"left": {
@ -26,7 +27,7 @@
"headline": "Plate-forme de coopération «Cercles Gradido»",
"text": "Cercles locaux, cercles d'études, projets, événements et congrès",
"allowed": {
"button": "Démarrer les cercles..."
"button": "Ouvrir les cercles..."
},
"not-allowed": {
"button": "Configurer..."
@ -35,19 +36,20 @@
"card-user-search": {
"headline": "Recherche géographique de membres (bêta)",
"allowed": {
"button": "Commencer la recherche de membres...",
"button": "Ouvrir la recherche de membres...",
"disabled-button": "GMS offline...",
"text": "Trouve des membres de toutes les communautés sur une carte."
},
"not-allowed": {
"button": "Configuration de l'emplacement...",
"text": "Trouve des membres de toutes les communautés sur une carte? Alors tu dois d'abord définir ton emplacement."
}
"button": "Indiquer ta position...",
"text": "Pour trouver d'autres membres près de chez toi, indique dès maintenant ta position sur la carte!"
},
"info": "Comment ça marche"
},
"circles": {
"headline": "Ensemble, nous nous soutenons mutuellement - attentifs à la culture du cercle.",
"text": "En cliquant sur le bouton, tu ouvres la plateforme de coopération dans une nouvelle fenêtre de navigation.",
"button": "Démarrer les Cercles..."
"button": "Ouvrir les Cercles..."
},
"community": {
"admins": "Administrateurs",
@ -69,7 +71,8 @@
"contribution": {
"activity": "Activité",
"alert": {
"answerQuestion": "S'il te plais répond à la question",
"answerQuestion": "Il y a un nouveau message concernant cette contribution.",
"answerQuestionToast": "Vous avez de nouveaux messages.",
"communityNoteList": "Vous trouverez ci-contre toutes les contributions versées et certifiées de tous les membres de cette communauté.",
"confirm": "Approuvé",
"deleted": "Supprimé",
@ -247,6 +250,7 @@
"recruited-member": "Membre invité"
},
"h": "h",
"info": "Information",
"language": "Langage",
"link-load": "Enregistrer le dernier lien | Enregistrer les derniers {n} liens",
"link-load-more": "Enregistrer plus de {n} liens",
@ -274,7 +278,6 @@
"navigation": {
"admin_area": "Partie administrative",
"community": "Communauté",
"info": "Information",
"logout": "Déconnexion",
"overview": "Aperçu",
"send": "Envoyer",

View File

@ -7,6 +7,7 @@
"GDT": "GDT",
"PersonalDetails": "Persoonlijke gegevens",
"advanced-calculation": "Voorcalculatie",
"answerNow": "→ Nu antwoorden!",
"asterisks": "****",
"auth": {
"left": {
@ -24,7 +25,7 @@
"headline": "Samenwerkingsplatform “Gradido Kringen”",
"text": "Lokale kringen, studiekringen, projecten, evenementen en congressen",
"allowed": {
"button": "Kringen starten..."
"button": "Kringen openen..."
},
"not-allowed": {
"button": "Configureren..."
@ -33,19 +34,20 @@
"card-user-search": {
"headline": "Geografisch leden zoeken (bèta)",
"allowed": {
"button": "Zoeken naar leden starten...",
"button": "Leden zoeken openen...",
"disabled-button": "GMS offline...",
"text": "Vind leden van alle gemeenschappen op een kaart."
},
"not-allowed": {
"button": "Locatie instellen...",
"text": "Vind leden van alle gemeenschappen op een kaart? Dan moet je eerst je locatie instellen."
}
"button": "Locatie invoeren",
"text": "Om andere leden in jouw omgeving te vinden, voer nu je locatie in op de kaart!"
},
"info": "Zo gaat dat"
},
"circles": {
"headline": "Samen ondersteunen we elkaar - mindful in de cirkelcultuur.",
"text": "Klik op de knop om het samenwerkingsplatform te openen in een nieuw browservenster.",
"button": "Cirkels starten..."
"button": "Cirkels openen..."
},
"community": {
"admins": "Beheerders",
@ -69,7 +71,8 @@
"contribution": {
"activity": "Activiteit",
"alert": {
"answerQuestion": "Please answer the question",
"answerQuestion": "Er is een nieuw bericht over deze bijdrage.",
"answerQuestionToast": "U hebt nieuwe berichten.",
"communityNoteList": "Hier vind je alle ingediende en bevestigde bijdragen van alle leden uit deze gemeenschap.",
"confirm": "bevestigt",
"denied": "afgewezen",
@ -239,6 +242,7 @@
"raise": "Verhoging",
"recruited-member": "Uitgenodigd lid"
},
"info": "Informatie",
"language": "Taal",
"link-load": "de laatste link herladen | de laatste links herladen",
"link-load-more": "verdere {n} links herladen",
@ -265,7 +269,6 @@
"navigation": {
"admin_area": "Beheerder",
"community": "Gemeenschap",
"info": "Informatie",
"logout": "Afmelden",
"members_area": "Ledenbestand",
"overview": "Overzicht",

View File

@ -11,9 +11,7 @@ import nl from '@vee-validate/i18n/dist/locale/nl.json'
import tr from '@vee-validate/i18n/dist/locale/tr.json'
import { useI18n } from 'vue-i18n'
// Email and username regex patterns remain the same
const EMAIL_REGEX =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
// username regex pattern remain the same
const USERNAME_REGEX = /^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/
export const loadAllRules = (i18nCallback, apollo) => {
@ -48,22 +46,6 @@ export const loadAllRules = (i18nCallback, apollo) => {
defineRule('max', max)
// ------ Custom rules ------
defineRule('gddSendAmount', (value, { min, max }) => {
value = value.replace(',', '.')
return value.match(/^[0-9]+(\.[0-9]{0,2})?$/) && Number(value) >= min && Number(value) <= max
? true
: i18nCallback.t('form.validation.gddSendAmount', {
min: i18nCallback.n(min, 'ungroupedDecimal'),
max: i18nCallback.n(max, 'ungroupedDecimal'),
})
})
defineRule('gddCreationTime', (value, { min, max }) => {
return value >= min && value <= max
? true
: i18nCallback.t('form.validation.gddCreationTime', { min, max })
})
defineRule('is_not', (value, [otherValue]) => {
return value !== otherValue
? true
@ -122,13 +104,4 @@ export const loadAllRules = (i18nCallback, apollo) => {
})
return data.checkUsername || i18nCallback.t('form.validation.username-unique')
})
defineRule('validIdentifier', (value) => {
const isEmail = !!EMAIL_REGEX.test(value)
const isUsername = !!value.match(USERNAME_REGEX)
const isGradidoId = validateUuid(value) && versionUuid(value) === 4
return (
isEmail || isUsername || isGradidoId || i18nCallback.t('form.validation.valid-identifier')
)
})
}

View File

@ -1,4 +1,10 @@
import { string } from 'yup'
import { validate as validateUuid, version as versionUuid } from 'uuid'
// Email and username regex patterns remain the same
const EMAIL_REGEX =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
const USERNAME_REGEX = /^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/
// TODO: only needed for grace period, before all inputs updated for using veeValidate + yup
export const isLanguageKey = (str) =>
@ -16,6 +22,16 @@ export const translateYupErrorString = (error, t) => {
}
export const memo = string()
.required('contribution.yourActivity')
.required('form.validation.memo.required')
.min(5, ({ min }) => ({ key: 'form.validation.memo.min', values: { min } }))
.max(255, ({ max }) => ({ key: 'form.validation.memo.max', values: { max } }))
.max(512, ({ max }) => ({ key: 'form.validation.memo.max', values: { max } }))
export const identifier = string()
.required('form.validation.identifier.required')
.test('valid-identifier', 'form.validation.identifier.typeError', (value) => {
const isEmail = !!EMAIL_REGEX.test(value)
const isUsername = !!value.match(USERNAME_REGEX)
// TODO: use valibot and rules from shared
const isGradidoId = validateUuid(value) && versionUuid(value) === 4
return isEmail || isUsername || isGradidoId
})

File diff suppressed because it is too large Load Diff

View File

@ -43,6 +43,31 @@ server {
proxy_redirect off;
}
# Well-Known for openid connect
location /.well-known {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_pass http://backend:4000/realms/gradido/.well-known;
proxy_redirect off;
}
location /realms/gradido {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_pass http://backend:4000/realms/gradido;
proxy_redirect off;
}
# Admin Frontend
location /admin {
proxy_http_version 1.1;

View File

@ -1,6 +1,6 @@
{
"name": "gradido",
"version": "2.6.0",
"version": "2.6.1",
"description": "Gradido",
"main": "index.js",
"repository": "git@github.com:gradido/gradido.git",

View File

@ -1,6 +1,6 @@
{
"name": "shared",
"version": "2.6.0",
"version": "2.6.1",
"description": "Gradido Shared Code, Low-Level Shared Code, without dependencies on other modules",
"main": "./build/index.js",
"types": "./src/index.ts",

View File

@ -10,3 +10,6 @@ export * from './jwt/payloadtypes/JwtPayloadType'
export * from './jwt/payloadtypes/OpenConnectionJwtPayloadType'
export * from './jwt/payloadtypes/OpenConnectionCallbackJwtPayloadType'
export * from './jwt/payloadtypes/RedeemJwtPayloadType'
export * from './jwt/payloadtypes/SendCoinsJwtPayloadType'
export * from './jwt/payloadtypes/SendCoinsResponseJwtPayloadType'

View File

@ -0,0 +1,43 @@
import { JwtPayloadType } from './JwtPayloadType'
import { Decimal } from 'decimal.js-light'
export class SendCoinsJwtPayloadType extends JwtPayloadType {
static SEND_COINS_TYPE = 'send-coins'
recipientCommunityUuid: string
recipientUserIdentifier: string
creationDate: string
amount: Decimal
memo: string
senderCommunityUuid: string
senderUserUuid: string
senderUserName: string
senderAlias?: string | null
constructor(
handshakeID: string,
recipientCommunityUuid: string,
recipientUserIdentifier: string,
creationDate: string,
amount: Decimal,
memo: string,
senderCommunityUuid: string,
senderUserUuid: string,
senderUserName: string,
senderAlias?: string | null,
) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
super(handshakeID)
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
this.tokentype = SendCoinsJwtPayloadType.SEND_COINS_TYPE
this.recipientCommunityUuid = recipientCommunityUuid
this.recipientUserIdentifier = recipientUserIdentifier
this.creationDate = creationDate
this.amount = amount
this.memo = memo
this.senderCommunityUuid = senderCommunityUuid
this.senderUserUuid = senderUserUuid
this.senderUserName = senderUserName
this.senderAlias = senderAlias
}
}

View File

@ -0,0 +1,30 @@
import { JwtPayloadType } from './JwtPayloadType'
export class SendCoinsResponseJwtPayloadType extends JwtPayloadType {
static SEND_COINS_RESPONSE_TYPE = 'send-coins-response'
vote: boolean
recipGradidoID: string | null
recipFirstName: string | null
recipLastName: string | null
recipAlias: string | null
constructor(
handshakeID: string,
vote: boolean,
recipGradidoID: string | null,
recipFirstName: string | null,
recipLastName: string | null,
recipAlias: string | null,
) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
super(handshakeID)
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
this.tokentype = SendCoinsResponseJwtPayloadType.SEND_COINS_RESPONSE_TYPE
this.vote = vote
this.recipGradidoID = recipGradidoID
this.recipFirstName = recipFirstName
this.recipLastName = recipLastName
this.recipAlias = recipAlias
}
}

View File

@ -1,6 +1,7 @@
import { string } from 'zod'
import { string, number } from 'zod'
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 urlSchema = string().url()
export const uint32Schema = number().positive().lte(4294967295)

2439
yarn.lock

File diff suppressed because it is too large Load Diff