mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge branch 'master' into refactor_dlt_connector_modern_stack
This commit is contained in:
commit
b17c381f6c
11
.github/workflows/publish.yml
vendored
11
.github/workflows/publish.yml
vendored
@ -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]
|
||||
|
||||
2
.github/workflows/test_database.yml
vendored
2
.github/workflows/test_database.yml
vendored
@ -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'
|
||||
|
||||
17
CHANGELOG.md
17
CHANGELOG.md
@ -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)
|
||||
|
||||
43
README.md
43
README.md
@ -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.
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -28,6 +28,9 @@
|
||||
<BListGroupItem>
|
||||
{{ $t('federation.publicKey') }} {{ item.publicKey }}
|
||||
</BListGroupItem>
|
||||
<BListGroupItem v-if="item.hieroTopicId && item.foreign">
|
||||
{{ $t('federation.hieroTopicId') }} {{ 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') }} </p>
|
||||
<span class="d-block" style="overflow-x: auto">{{ gmsApiKey }}</span>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<p style="text-wrap: nowrap">{{ $t('federation.hieroTopicId') }} </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>
|
||||
|
||||
@ -15,6 +15,7 @@ export const allCommunities = gql`
|
||||
creationDate
|
||||
createdAt
|
||||
updatedAt
|
||||
hieroTopicId
|
||||
federatedCommunities {
|
||||
id
|
||||
apiVersion
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
7205
admin/yarn.lock
7205
admin/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
||||
@ -1 +1,3 @@
|
||||
export const LOG4JS_BASE_CATEGORY_NAME = 'backend'
|
||||
export const FRONTEND_LOGIN_ROUTE = 'login'
|
||||
export const GRADIDO_REALM = 'gradido'
|
||||
@ -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 },
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { gql } from 'graphql-request'
|
||||
|
||||
export const revertSendCoins = gql`
|
||||
mutation ($args: SendCoinsArgs!) {
|
||||
mutation ($args: EncryptedTransferArgs!) {
|
||||
revertSendCoins(data: $args)
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { gql } from 'graphql-request'
|
||||
|
||||
export const revertSettledSendCoins = gql`
|
||||
mutation ($args: SendCoinsArgs!) {
|
||||
mutation ($args: EncryptedTransferArgs!) {
|
||||
revertSettledSendCoins(data: $args)
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { gql } from 'graphql-request'
|
||||
|
||||
export const settleSendCoins = gql`
|
||||
mutation ($args: SendCoinsArgs!) {
|
||||
mutation ($args: EncryptedTransferArgs!) {
|
||||
settleSendCoins(data: $args)
|
||||
}
|
||||
`
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
`
|
||||
*/
|
||||
|
||||
@ -21,7 +21,7 @@ export class EditCommunityInput {
|
||||
location?: Location | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@isValidHieroId()
|
||||
topicId?: string | null
|
||||
|
||||
hieroTopicId?: string | null
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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-'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
63
backend/src/openIDConnect/index.ts
Normal file
63
backend/src/openIDConnect/index.ts
Normal 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' })
|
||||
}
|
||||
}
|
||||
@ -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(),
|
||||
|
||||
92
bun.lock
92
bun.lock
@ -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=="],
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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`;')
|
||||
}
|
||||
20
database/migration/migrations/0093-increase_memo_to_512.ts
Normal file
20
database/migration/migrations/0093-increase_memo_to_512.ts
Normal 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;')
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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 })
|
||||
|
||||
@ -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 })
|
||||
|
||||
@ -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 })
|
||||
|
||||
@ -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' })
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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/",
|
||||
|
||||
@ -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)
|
||||
|
||||
3807
dht-node/yarn.lock
3807
dht-node/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -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
1907
e2e-tests/bun.lock
Normal file
File diff suppressed because it is too large
Load Diff
25
e2e-tests/playwright/typescript/bun.lock
Normal file
25
e2e-tests/playwright/typescript/bun.lock
Normal 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=="],
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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', {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
4820
federation/yarn.lock
4820
federation/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
}))
|
||||
|
||||
|
||||
@ -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())
|
||||
}),
|
||||
|
||||
@ -22,7 +22,7 @@ const i18n = createI18n({
|
||||
},
|
||||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
moderatorChat: 'Chat',
|
||||
Chat: 'Chat',
|
||||
},
|
||||
},
|
||||
datetimeFormats: {
|
||||
|
||||
@ -22,7 +22,7 @@ const i18n = createI18n({
|
||||
},
|
||||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
moderatorChat: 'Chat',
|
||||
Chat: 'Chat',
|
||||
},
|
||||
},
|
||||
datetimeFormats: {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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')
|
||||
}
|
||||
|
||||
@ -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'])
|
||||
})
|
||||
})
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
@ -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>
|
||||
92
frontend/src/components/Inputs/ValidatedInput.spec.js
Normal file
92
frontend/src/components/Inputs/ValidatedInput.spec.js
Normal 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')
|
||||
})
|
||||
})
|
||||
@ -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>
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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',
|
||||
})
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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')
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@ -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
|
||||
})
|
||||
|
||||
7679
frontend/yarn.lock
7679
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
43
shared/src/jwt/payloadtypes/SendCoinsJwtPayloadType.ts
Normal file
43
shared/src/jwt/payloadtypes/SendCoinsJwtPayloadType.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
Loading…
x
Reference in New Issue
Block a user