Merge branch 'master' into refactor_drizzle_orm_for_project_branding

This commit is contained in:
einhornimmond 2026-03-09 12:50:48 +01:00 committed by GitHub
commit db9c380bec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
84 changed files with 2038 additions and 619 deletions

View File

@ -1,7 +1,7 @@
name: gradido publish CI name: gradido publish CI
on: on:
push: pull_request:
branches: branches:
- master - master
@ -10,7 +10,7 @@ jobs:
# JOB: DOCKER BUILD PRODUCTION FRONTEND ###################################### # JOB: DOCKER BUILD PRODUCTION FRONTEND ######################################
############################################################################## ##############################################################################
build_production_frontend: build_production_frontend:
if: startsWith(github.event.head_commit.message, 'chore(release):') if: startsWith(github.event.pull_request.title, 'chore(release):')
name: Docker Build Production - Frontend name: Docker Build Production - Frontend
runs-on: ubuntu-latest runs-on: ubuntu-latest
#needs: [nothing] #needs: [nothing]

View File

@ -4,8 +4,26 @@ 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). Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v2.7.4](https://github.com/gradido/gradido/compare/v2.7.3...v2.7.4)
- feat(workflow): update .env.dist in deploy and make install script more idempotent [`#3593`](https://github.com/gradido/gradido/pull/3593)
- feat(admin): load moderator names for contribution list [`#3599`](https://github.com/gradido/gradido/pull/3599)
- fix(other): email mime types [`#3601`](https://github.com/gradido/gradido/pull/3601)
- fix(database): use connection pool for drizzle orm [`#3600`](https://github.com/gradido/gradido/pull/3600)
- feat(admin): make deleted contributions better recognisable with Deuteranopie [`#3597`](https://github.com/gradido/gradido/pull/3597)
- fix(workflow): rewrite sort.sh as sortLocales.ts [`#3598`](https://github.com/gradido/gradido/pull/3598)
- refactor(database): add drizzleOrm, use it in openaiThreads [`#3595`](https://github.com/gradido/gradido/pull/3595)
- fix(other): fix biome config [`#3592`](https://github.com/gradido/gradido/pull/3592)
- feat(other): reduce github worker count [`#3591`](https://github.com/gradido/gradido/pull/3591)
- fix(other): removed phantom line, aligned env and compose [`#3570`](https://github.com/gradido/gradido/pull/3570)
- feat(dlt): migrate database transaction to dlt transactions [`#3571`](https://github.com/gradido/gradido/pull/3571)
- feat(dlt): add inspector as submodule, update nginx config to serve inspector and gradido node [`#3566`](https://github.com/gradido/gradido/pull/3566)
- chore(release): v2.7.3 [`#3590`](https://github.com/gradido/gradido/pull/3590)
#### [v2.7.3](https://github.com/gradido/gradido/compare/v2.7.2...v2.7.3) #### [v2.7.3](https://github.com/gradido/gradido/compare/v2.7.2...v2.7.3)
> 4 December 2025
- feat(admin): show user registered at in admin [`#3589`](https://github.com/gradido/gradido/pull/3589) - feat(admin): show user registered at in admin [`#3589`](https://github.com/gradido/gradido/pull/3589)
- feat(backend): 3573 feature introduce distributed semaphore base on redis [`#3580`](https://github.com/gradido/gradido/pull/3580) - feat(backend): 3573 feature introduce distributed semaphore base on redis [`#3580`](https://github.com/gradido/gradido/pull/3580)
- fix(workflow): make deployment install script more robust [`#3588`](https://github.com/gradido/gradido/pull/3588) - fix(workflow): make deployment install script more robust [`#3588`](https://github.com/gradido/gradido/pull/3588)

View File

@ -1,5 +1,3 @@
CONFIG_VERSION=$ADMIN_CONFIG_VERSION
COMMUNITY_HOST=$COMMUNITY_HOST COMMUNITY_HOST=$COMMUNITY_HOST
URL_PROTOCOL=$URL_PROTOCOL URL_PROTOCOL=$URL_PROTOCOL
WALLET_AUTH_PATH=$WALLET_AUTH_PATH WALLET_AUTH_PATH=$WALLET_AUTH_PATH

View File

@ -3,7 +3,7 @@
"description": "Administration Interface for Gradido", "description": "Administration Interface for Gradido",
"main": "index.js", "main": "index.js",
"author": "Gradido Academy - https://www.gradido.net", "author": "Gradido Academy - https://www.gradido.net",
"version": "2.7.3", "version": "2.7.4",
"license": "Apache-2.0", "license": "Apache-2.0",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@ -18,8 +18,8 @@
"test:coverage": "cross-env TZ=UTC vitest run --coverage", "test:coverage": "cross-env TZ=UTC vitest run --coverage",
"test:debug": "cross-env TZ=UTC node --inspect-brk ./node_modules/vitest/vitest.mjs", "test:debug": "cross-env TZ=UTC node --inspect-brk ./node_modules/vitest/vitest.mjs",
"test:watch": "cross-env TZ=UTC vitest", "test:watch": "cross-env TZ=UTC vitest",
"locales": "scripts/sort.sh", "locales": "bun scripts/sortLocales.ts",
"locales:fix": "scripts/sort.sh --fix", "locales:fix": "bun scripts/sortLocales.ts --fix",
"clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo" "clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo"
}, },
"dependencies": { "dependencies": {

View File

@ -0,0 +1,51 @@
#!/usr/bin/env bun
import { readdir, readFile, writeFile } from 'fs/promises'
import { join } from 'path'
const ROOT_DIR = join(import.meta.dir, '..')
const LOCALES_DIR = join(ROOT_DIR, 'src', 'locales')
const FIX = process.argv.includes('--fix')
function sortObject(value: any): any {
if (Array.isArray(value)) {
return value.map(sortObject)
}
if (value && typeof value === 'object') {
return Object.keys(value)
.sort()
.reduce<Record<string, any>>((acc, key) => {
acc[key] = sortObject(value[key])
return acc
}, {})
}
return value
}
let exitCode = 0
const files = (await readdir(LOCALES_DIR))
.filter(f => f.endsWith('.json'))
for (const file of files) {
const path = join(LOCALES_DIR, file)
const originalText = await readFile(path, 'utf8')
const originalJson = JSON.parse(originalText)
const sortedJson = sortObject(originalJson)
const sortedText = JSON.stringify(sortedJson, null, 2) + '\n'
if (originalText !== sortedText) {
if (FIX) {
await writeFile(path, sortedText)
} else {
console.error(`${file} is not sorted by keys`)
exitCode = 1
}
}
}
process.exit(exitCode)

View File

@ -6,7 +6,7 @@
:icon="type === 'PageCreationConfirm' ? 'x' : 'eye-slash-fill'" :icon="type === 'PageCreationConfirm' ? 'x' : 'eye-slash-fill'"
aria-label="Help" aria-label="Help"
></b-icon> ></b-icon>
{{ $t('hide_details') }} {{ row.item.user.firstName }} {{ row.item.user.lastName }} {{ $t('hide_details') }}
</b-button> </b-button>
</b-card> </b-card>
</template> </template>

View File

@ -14,7 +14,13 @@
<IBiBellFill v-else-if="row.item.contributionStatus === 'PENDING'" /> <IBiBellFill v-else-if="row.item.contributionStatus === 'PENDING'" />
<IBiCheck v-else-if="row.item.contributionStatus === 'CONFIRMED'" /> <IBiCheck v-else-if="row.item.contributionStatus === 'CONFIRMED'" />
<IBiXCircle v-else-if="row.item.contributionStatus === 'DENIED'" /> <IBiXCircle v-else-if="row.item.contributionStatus === 'DENIED'" />
<IBiTrash v-else-if="row.item.contributionStatus === 'DELETED'" /> <IBiTrash
v-else-if="row.item.contributionStatus === 'DELETED'"
class="p-1"
width="24"
height="24"
style="background-color: #dc3545; color: white"
/>
</template> </template>
<template #cell(bookmark)="row"> <template #cell(bookmark)="row">
<div v-if="!myself(row.item)"> <div v-if="!myself(row.item)">
@ -28,11 +34,20 @@
</BButton> </BButton>
</div> </div>
</template> </template>
<template #cell(name)="row">
<span v-if="row.item.user">
{{ row.item.user.firstName }} {{ row.item.user.lastName }}
<small v-if="row.item.user.alias">
<hr />
{{ row.item.user.alias }}
</small>
</span>
</template>
<template #cell(memo)="row"> <template #cell(memo)="row">
{{ row.value }} {{ row.value }}
<small v-if="row.item.updatedBy > 0"> <small v-if="isAddCommentToMemo(row.item)" class="no-select">
<hr /> <hr />
{{ $t('moderator.memo-modified') }} {{ getMemoComment(row.item) }}
</small> </small>
</template> </template>
<template #cell(editCreation)="row"> <template #cell(editCreation)="row">
@ -134,6 +149,7 @@
import RowDetails from '../RowDetails' import RowDetails from '../RowDetails'
import EditCreationFormular from '../EditCreationFormular' import EditCreationFormular from '../EditCreationFormular'
import ContributionMessagesList from '../ContributionMessages/ContributionMessagesList' import ContributionMessagesList from '../ContributionMessages/ContributionMessagesList'
import { useDateFormatter } from '@/composables/useDateFormatter'
const iconMap = { const iconMap = {
IN_PROGRESS: 'question-square', IN_PROGRESS: 'question-square',
@ -189,6 +205,7 @@ export default {
this.removeClipboardListener() this.removeClipboardListener()
}, },
methods: { methods: {
...useDateFormatter(),
myself(item) { myself(item) {
return item.userId === this.$store.state.moderator.id return item.userId === this.$store.state.moderator.id
}, },
@ -229,6 +246,36 @@ export default {
this.creationUserData = row.item this.creationUserData = row.item
} }
}, },
isAddCommentToMemo(item) {
return item.closedBy > 0 || item.moderatorId > 0 || item.updatedBy > 0
},
getMemoComment(item) {
let comment = ''
if (item.closedBy > 0) {
if (item.contributionStatus === 'CONFIRMED') {
comment = this.$t('contribution.confirmedBy', { name: item.closedByUserName })
} else if (item.contributionStatus === 'DENIED') {
comment = this.$t('contribution.deniedBy', { name: item.closedByUserName })
} else if (item.contributionStatus === 'DELETED') {
comment = this.$t('contribution.deletedBy', { name: item.closedByUserName })
}
}
if (item.updatedBy > 0) {
if (comment.length) {
comment += ' | '
}
comment += this.$t('moderator.memo-modified', { name: item.updatedByUserName })
}
if (item.moderatorId > 0) {
if (comment.length) {
comment += ' | '
}
comment += this.$t('contribution.createdBy', { name: item.moderatorUserName })
}
return comment
},
addClipboardListener() { addClipboardListener() {
document.addEventListener('copy', this.handleCopy) document.addEventListener('copy', this.handleCopy)
}, },
@ -254,4 +301,9 @@ export default {
background-color: #e1a908; background-color: #e1a908;
border-color: #e1a908; border-color: #e1a908;
} }
.table-danger {
--bs-table-bg: #e78d8d;
--bs-table-striped-bg: #e57373;
}
</style> </style>

View File

@ -1,10 +1,14 @@
export const useDateFormatter = () => { export const useDateFormatter = () => {
const formatDateFromDateTime = (datetimeString) => { const formatDateFromDateTime = (datetimeString) => {
if (!datetimeString || !datetimeString?.includes('T')) return datetimeString if (!datetimeString || !datetimeString?.includes('T')) {
return datetimeString
}
return datetimeString.split('T')[0] return datetimeString.split('T')[0]
} }
const formatDateOrDash = (value) => (value ? new Date(value).toLocaleDateString() : '—')
return { return {
formatDateFromDateTime, formatDateFromDateTime,
formatDateOrDash,
} }
} }

View File

@ -19,19 +19,18 @@ query adminListContributions(
} }
amount amount
memo memo
createdAt closedAt
closedBy
closedByUserName
contributionDate contributionDate
confirmedAt createdAt
confirmedBy
updatedAt updatedAt
updatedBy updatedBy
updatedByUserName
contributionStatus contributionStatus
messagesCount messagesCount
deniedAt
deniedBy
deletedAt
deletedBy
moderatorId moderatorId
moderatorUserName
userId userId
resubmissionAt resubmissionAt
} }

View File

@ -3,8 +3,8 @@
"actions": "Aktionen", "actions": "Aktionen",
"ai": { "ai": {
"chat": "Chat", "chat": "Chat",
"chat-open": "Chat öffnen",
"chat-clear": "Chat-Verlauf löschen", "chat-clear": "Chat-Verlauf löschen",
"chat-open": "Chat öffnen",
"chat-placeholder": "Schreibe eine Nachricht...", "chat-placeholder": "Schreibe eine Nachricht...",
"chat-placeholder-loading": "Warte, ich denke nach...", "chat-placeholder-loading": "Warte, ich denke nach...",
"chat-thread-deleted": "Chatverlauf gelöscht", "chat-thread-deleted": "Chatverlauf gelöscht",
@ -16,6 +16,12 @@
"back": "zurück", "back": "zurück",
"change_user_role": "Nutzerrolle ändern", "change_user_role": "Nutzerrolle ändern",
"close": "Schließen", "close": "Schließen",
"contribution": {
"confirmedBy": "Bestätigt von {name}.",
"createdBy": "Erstellt von {name}.",
"deletedBy": "Gelöscht von {name}.",
"deniedBy": "Abgelehnt von {name}."
},
"contributionLink": { "contributionLink": {
"amount": "Betrag", "amount": "Betrag",
"changeSaved": "Änderungen gespeichert", "changeSaved": "Änderungen gespeichert",
@ -23,8 +29,8 @@
"contributionLinks": "Beitragslinks", "contributionLinks": "Beitragslinks",
"create": "Anlegen", "create": "Anlegen",
"cycle": "Zyklus", "cycle": "Zyklus",
"deleted": "Automatische Schöpfung gelöscht!",
"deleteNow": "Automatische Creations '{name}' wirklich löschen?", "deleteNow": "Automatische Creations '{name}' wirklich löschen?",
"deleted": "Automatische Schöpfung gelöscht!",
"maxPerCycle": "Wiederholungen", "maxPerCycle": "Wiederholungen",
"memo": "Nachricht", "memo": "Nachricht",
"name": "Name", "name": "Name",
@ -44,11 +50,12 @@
"validTo": "Enddatum" "validTo": "Enddatum"
}, },
"contributionMessagesForm": { "contributionMessagesForm": {
"resubmissionDateInPast": "Wiedervorlage Datum befindet sich in der Vergangenheit!", "hasRegisteredAt": "hat sich am {createdAt} registriert.",
"hasRegisteredAt": "hat sich am {createdAt} registriert." "resubmissionDateInPast": "Wiedervorlage Datum befindet sich in der Vergangenheit!"
}, },
"contributions": { "contributions": {
"all": "Alle", "all": "Alle",
"closed": "Geschlossen",
"confirms": "Bestätigt", "confirms": "Bestätigt",
"deleted": "Gelöscht", "deleted": "Gelöscht",
"denied": "Abgelehnt", "denied": "Abgelehnt",
@ -96,16 +103,16 @@
"coordinates": "Koordinaten:", "coordinates": "Koordinaten:",
"createdAt": "Erstellt am", "createdAt": "Erstellt am",
"gmsApiKey": "GMS API Key:", "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", "gradidoInstances": "Gradido Instanzen",
"hieroTopicId": "Hiero Topic ID:",
"lastAnnouncedAt": "letzte Bekanntgabe", "lastAnnouncedAt": "letzte Bekanntgabe",
"lastErrorAt": "Letzer Fehler am", "lastErrorAt": "Letzer Fehler am",
"name": "Name", "name": "Name",
"publicKey": "PublicKey:", "publicKey": "PublicKey:",
"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!",
"url": "Url", "url": "Url",
"verified": "Verifiziert", "verified": "Verifiziert",
"verifiedAt": "Verifiziert am" "verifiedAt": "Verifiziert am"
@ -134,8 +141,8 @@
} }
}, },
"goTo": { "goTo": {
"userSearch": "Zur Nutzersuche gehen", "humhubProfile": "Zum Humhub Profil gehen",
"humhubProfile": "Zum Humhub Profil gehen" "userSearch": "Zur Nutzersuche gehen"
}, },
"help": { "help": {
"help": "Hilfe", "help": "Hilfe",
@ -165,16 +172,16 @@
}, },
"moderator": { "moderator": {
"history": "Die Daten wurden geändert. Dies sind die alten Daten.", "history": "Die Daten wurden geändert. Dies sind die alten Daten.",
"show-submission-form": "warten auf Erinnerung?", "memo": "Text ändern",
"memo-modified": "Text von {name} bearbeitet.",
"memo-tooltip": "Den Beitragstext bearbeiten",
"message": "Nachricht",
"message-tooltip": "Nachricht an Benutzer schreiben",
"moderator": "Moderator", "moderator": "Moderator",
"notice": "Notiz", "notice": "Notiz",
"notice-tooltip": "Die Notiz ist nur für Moderatoren sichtbar", "notice-tooltip": "Die Notiz ist nur für Moderatoren sichtbar",
"memo": "Text ändern", "request": "Diese Nachricht ist nur für die Moderatoren sichtbar!",
"memo-tooltip": "Den Beitragstext bearbeiten", "show-submission-form": "warten auf Erinnerung?"
"memo-modified": "Text vom Moderator bearbeitet.",
"message": "Nachricht",
"message-tooltip": "Nachricht an Benutzer schreiben",
"request": "Diese Nachricht ist nur für die Moderatoren sichtbar!"
}, },
"name": "Name", "name": "Name",
"navbar": { "navbar": {
@ -233,14 +240,14 @@
"chosenSpace": "Gewählter Space: {space}", "chosenSpace": "Gewählter Space: {space}",
"created": "Neuer Projekt Branding Eintrag wurde erstellt.", "created": "Neuer Projekt Branding Eintrag wurde erstellt.",
"error": "Fehler beim Erstellen des Projekt Branding Eintrags: {message}", "error": "Fehler beim Erstellen des Projekt Branding Eintrags: {message}",
"newUserToSpace": "Benutzer hinzufügen?",
"newUserToSpaceTooltip": "Neue Benutzer automatisch zum Space hinzufügen, falls Space vorhanden",
"noAccessRightSpace": "Gewählter Space: {spaceId} (Keine Zugriffsrechte)", "noAccessRightSpace": "Gewählter Space: {spaceId} (Keine Zugriffsrechte)",
"openSpaceInHumhub": "In Humhub öffnen", "openSpaceInHumhub": "In Humhub öffnen",
"spaceId": "Humhub Space ID",
"selectSpace": "Humhub Space auswählen", "selectSpace": "Humhub Space auswählen",
"spaceId": "Humhub Space ID",
"title": "Projekt Brandings", "title": "Projekt Brandings",
"updated": "Projekt Branding Eintrag wurde aktualisiert.", "updated": "Projekt Branding Eintrag wurde aktualisiert."
"newUserToSpace": "Benutzer hinzufügen?",
"newUserToSpaceTooltip": "Neue Benutzer automatisch zum Space hinzufügen, falls Space vorhanden"
}, },
"redeemed": "eingelöst", "redeemed": "eingelöst",
"registered": "Registriert", "registered": "Registriert",

View File

@ -3,8 +3,8 @@
"actions": "Actions", "actions": "Actions",
"ai": { "ai": {
"chat": "Chat", "chat": "Chat",
"chat-open": "Open chat",
"chat-clear": "Clear chat", "chat-clear": "Clear chat",
"chat-open": "Open chat",
"chat-placeholder": "Type your message here...", "chat-placeholder": "Type your message here...",
"chat-placeholder-loading": "Wait, I think...", "chat-placeholder-loading": "Wait, I think...",
"chat-thread-deleted": "Chat thread has been deleted", "chat-thread-deleted": "Chat thread has been deleted",
@ -16,6 +16,12 @@
"back": "back", "back": "back",
"change_user_role": "Change user role", "change_user_role": "Change user role",
"close": "Close", "close": "Close",
"contribution": {
"confirmedBy": "Confirmed by {name}.",
"createdBy": "Created by {name}.",
"deletedBy": "Deleted by {name}.",
"deniedBy": "Rejected by {name}."
},
"contributionLink": { "contributionLink": {
"amount": "Amount", "amount": "Amount",
"changeSaved": "Changes saved", "changeSaved": "Changes saved",
@ -23,8 +29,8 @@
"contributionLinks": "Contribution Links", "contributionLinks": "Contribution Links",
"create": "Create", "create": "Create",
"cycle": "Cycle", "cycle": "Cycle",
"deleted": "Automatic creation deleted!",
"deleteNow": "Do you really delete automatic creations '{name}'?", "deleteNow": "Do you really delete automatic creations '{name}'?",
"deleted": "Automatic creation deleted!",
"maxPerCycle": "Repetition", "maxPerCycle": "Repetition",
"memo": "Memo", "memo": "Memo",
"name": "Name", "name": "Name",
@ -44,11 +50,12 @@
"validTo": "End-Date" "validTo": "End-Date"
}, },
"contributionMessagesForm": { "contributionMessagesForm": {
"resubmissionDateInPast": "Resubmission date is in the past!", "hasRegisteredAt": "registered on {createdAt}.",
"hasRegisteredAt": "registered on {createdAt}." "resubmissionDateInPast": "Resubmission date is in the past!"
}, },
"contributions": { "contributions": {
"all": "All", "all": "All",
"closed": "Closed",
"confirms": "Confirmed", "confirms": "Confirmed",
"deleted": "Deleted", "deleted": "Deleted",
"denied": "Rejected", "denied": "Rejected",
@ -96,16 +103,16 @@
"coordinates": "Coordinates:", "coordinates": "Coordinates:",
"createdAt": "Created At ", "createdAt": "Created At ",
"gmsApiKey": "GMS API Key:", "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", "gradidoInstances": "Gradido Instances",
"hieroTopicId": "Hiero Topic ID:",
"lastAnnouncedAt": "Last Announced", "lastAnnouncedAt": "Last Announced",
"lastErrorAt": "last error at", "lastErrorAt": "last error at",
"name": "Name", "name": "Name",
"publicKey": "PublicKey:", "publicKey": "PublicKey:",
"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!",
"url": "Url", "url": "Url",
"verified": "Verified", "verified": "Verified",
"verifiedAt": "Verified at" "verifiedAt": "Verified at"
@ -127,15 +134,15 @@
}, },
"geo-coordinates": { "geo-coordinates": {
"both-or-none": "Please enter both or none!", "both-or-none": "Please enter both or none!",
"label": "geo-coordinates",
"format": "{latitude}, {longitude}", "format": "{latitude}, {longitude}",
"label": "geo-coordinates",
"latitude-longitude-smart": { "latitude-longitude-smart": {
"describe": "Automatically splits coordinates in the format 'latitude, longitude'. Simply enter your coordinates from Google Maps here, for example: 49.28187664243721, 9.740672183943639." "describe": "Automatically splits coordinates in the format 'latitude, longitude'. Simply enter your coordinates from Google Maps here, for example: 49.28187664243721, 9.740672183943639."
} }
}, },
"goTo": { "goTo": {
"userSearch": "Go to user search", "humhubProfile": "Go to Humhub profile",
"humhubProfile": "Go to Humhub profile" "userSearch": "Go to user search"
}, },
"help": { "help": {
"help": "Help", "help": "Help",
@ -165,16 +172,16 @@
}, },
"moderator": { "moderator": {
"history": "The data has been changed. This is the old data.", "history": "The data has been changed. This is the old data.",
"show-submission-form": "wait for reminder?", "memo": "Edit text",
"memo-modified": "Text edited by {name}",
"memo-tooltip": "Edit the text of the contribution",
"message": "Message",
"message-tooltip": "Write message to user",
"moderator": "Moderator", "moderator": "Moderator",
"notice": "Note", "notice": "Note",
"notice-tooltip": "The note is only visible to moderators", "notice-tooltip": "The note is only visible to moderators",
"memo": "Edit text", "request": "This message is only visible to the moderators!",
"memo-tooltip": "Edit the text of the contribution", "show-submission-form": "wait for reminder?"
"memo-modified": "Text edited by moderator",
"message": "Message",
"message-tooltip": "Write message to user",
"request": "This message is only visible to the moderators!"
}, },
"name": "Name", "name": "Name",
"navbar": { "navbar": {
@ -233,14 +240,14 @@
"chosenSpace": "Choosen Humhub Space: {space}", "chosenSpace": "Choosen Humhub Space: {space}",
"created": "New project branding entry has been created.", "created": "New project branding entry has been created.",
"error": "Error when creating the project branding entry: {message}", "error": "Error when creating the project branding entry: {message}",
"newUserToSpace": "Add user?",
"newUserToSpaceTooltip": "The hours should contain a maximum of two decimal places",
"noAccessRightSpace": "Selected space: {spaceId} (No access rights)", "noAccessRightSpace": "Selected space: {spaceId} (No access rights)",
"openSpaceInHumhub": "Open in Humhub", "openSpaceInHumhub": "Open in Humhub",
"spaceId": "Humhub Space ID",
"selectSpace": "Select Humhub Space", "selectSpace": "Select Humhub Space",
"spaceId": "Humhub Space ID",
"title": "Project Branding", "title": "Project Branding",
"updated": "Project branding entry has been updated.", "updated": "Project branding entry has been updated."
"newUserToSpace": "Add user?",
"newUserToSpaceTooltip": "The hours should contain a maximum of two decimal places"
}, },
"redeemed": "redeemed", "redeemed": "redeemed",
"registered": "Registered", "registered": "Registered",

View File

@ -108,6 +108,7 @@ import { confirmContribution } from '../graphql/confirmContribution'
import { denyContribution } from '../graphql/denyContribution' import { denyContribution } from '../graphql/denyContribution'
import { getContribution } from '../graphql/getContribution' import { getContribution } from '../graphql/getContribution'
import { useAppToast } from '@/composables/useToast' import { useAppToast } from '@/composables/useToast'
import { useDateFormatter } from '@/composables/useDateFormatter'
import CONFIG from '@/config' import CONFIG from '@/config'
const FILTER_TAB_MAP = [ const FILTER_TAB_MAP = [
@ -134,9 +135,10 @@ const query = ref('')
const noHashtag = ref(null) const noHashtag = ref(null)
const hideResubmissionModel = ref(true) const hideResubmissionModel = ref(true)
const formatDateOrDash = (value) => (value ? new Date(value).toLocaleDateString() : '—') const { formatDateOrDash } = useDateFormatter()
const baseFields = { const baseFields = {
name: { key: 'name', label: t('name'), class: 'no-select' },
firstName: { key: 'user.firstName', label: t('firstname'), class: 'no-select' }, firstName: { key: 'user.firstName', label: t('firstname'), class: 'no-select' },
lastName: { key: 'user.lastName', label: t('lastname'), class: 'no-select' }, lastName: { key: 'user.lastName', label: t('lastname'), class: 'no-select' },
amount: { key: 'amount', label: t('creation'), formatter: (value) => value + ' GDD' }, amount: { key: 'amount', label: t('creation'), formatter: (value) => value + ' GDD' },
@ -153,13 +155,12 @@ const baseFields = {
class: 'no-select', class: 'no-select',
formatter: formatDateOrDash, formatter: formatDateOrDash,
}, },
confirmedAt: { closedAt: {
key: 'confirmedAt', key: 'closedAt',
label: t('contributions.confirms'), label: t('contributions.closed'),
class: 'no-select', class: 'no-select',
formatter: formatDateOrDash, formatter: formatDateOrDash,
}, },
confirmedBy: { key: 'confirmedBy', label: t('moderator.moderator'), class: 'no-select' },
} }
const fields = computed( const fields = computed(
@ -169,70 +170,52 @@ const fields = computed(
[ [
{ key: 'bookmark', label: t('delete') }, { key: 'bookmark', label: t('delete') },
{ key: 'deny', label: t('deny') }, { key: 'deny', label: t('deny') },
baseFields.firstName, baseFields.name,
baseFields.lastName,
baseFields.amount, baseFields.amount,
baseFields.memo, baseFields.memo,
baseFields.contributionDate, baseFields.contributionDate,
{ key: 'moderatorId', label: t('moderator.moderator'), class: 'no-select' },
{ key: 'editCreation', label: t('details') }, { key: 'editCreation', label: t('details') },
{ key: 'confirm', label: t('save') }, { key: 'confirm', label: t('save') },
], ],
// confirmed contributions // confirmed contributions
[ [
baseFields.firstName, baseFields.name,
baseFields.lastName,
baseFields.amount, baseFields.amount,
baseFields.memo, baseFields.memo,
baseFields.contributionDate, baseFields.contributionDate,
baseFields.createdAt, baseFields.createdAt,
baseFields.confirmedAt, baseFields.closedAt,
baseFields.confirmedBy,
{ key: 'chatCreation', label: t('details') }, { key: 'chatCreation', label: t('details') },
], ],
// denied contributions // denied contributions
[ [
baseFields.firstName, baseFields.name,
baseFields.lastName,
baseFields.amount, baseFields.amount,
baseFields.memo, baseFields.memo,
baseFields.contributionDate, baseFields.contributionDate,
baseFields.createdAt, baseFields.createdAt,
{ baseFields.closedAt,
key: 'deniedAt',
label: t('contributions.denied'),
formatter: formatDateOrDash,
},
{ key: 'deniedBy', label: t('moderator.moderator') },
{ key: 'chatCreation', label: t('details') }, { key: 'chatCreation', label: t('details') },
], ],
// deleted contributions // deleted contributions
[ [
baseFields.firstName, baseFields.name,
baseFields.lastName,
baseFields.amount, baseFields.amount,
baseFields.memo, baseFields.memo,
baseFields.contributionDate, baseFields.contributionDate,
baseFields.createdAt, baseFields.createdAt,
{ baseFields.closedAt,
key: 'deletedAt',
label: t('contributions.deleted'),
formatter: formatDateOrDash,
},
{ key: 'deletedBy', label: t('moderator.moderator') },
{ key: 'chatCreation', label: t('details') }, { key: 'chatCreation', label: t('details') },
], ],
// all contributions // all contributions
[ [
{ key: 'contributionStatus', label: t('status') }, { key: 'contributionStatus', label: t('status') },
baseFields.firstName, baseFields.name,
baseFields.lastName,
baseFields.amount, baseFields.amount,
baseFields.memo, baseFields.memo,
baseFields.contributionDate, baseFields.contributionDate,
baseFields.createdAt, baseFields.createdAt,
baseFields.confirmedAt, baseFields.closedAt,
baseFields.confirmedBy,
{ key: 'chatCreation', label: t('details') }, { key: 'chatCreation', label: t('details') },
], ],
][tabIndex.value], ][tabIndex.value],

View File

@ -1,5 +1,3 @@
# must match the CONFIG_VERSION.EXPECTED definition in scr/config/index.ts
# Server # Server
JWT_SECRET=$JWT_SECRET JWT_SECRET=$JWT_SECRET
JWT_EXPIRES_IN=$JWT_EXPIRES_IN JWT_EXPIRES_IN=$JWT_EXPIRES_IN

View File

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

View File

@ -1,6 +1,6 @@
import { ContributionStatus } from '@enum/ContributionStatus'
import { Contribution as DbContribution } from 'database' import { Contribution as DbContribution } from 'database'
import { Field, Int, ObjectType } from 'type-graphql' import { Field, Int, ObjectType } from 'type-graphql'
import { UnconfirmedContribution } from './UnconfirmedContribution' import { UnconfirmedContribution } from './UnconfirmedContribution'
@ObjectType() @ObjectType()
@ -8,6 +8,7 @@ export class Contribution extends UnconfirmedContribution {
constructor(dbContribution: DbContribution) { constructor(dbContribution: DbContribution) {
super(dbContribution) super(dbContribution)
this.createdAt = dbContribution.createdAt this.createdAt = dbContribution.createdAt
this.moderatorId = dbContribution.moderatorId
this.confirmedAt = dbContribution.confirmedAt this.confirmedAt = dbContribution.confirmedAt
this.confirmedBy = dbContribution.confirmedBy this.confirmedBy = dbContribution.confirmedBy
this.contributionDate = dbContribution.contributionDate this.contributionDate = dbContribution.contributionDate
@ -19,11 +20,36 @@ export class Contribution extends UnconfirmedContribution {
this.updatedAt = dbContribution.updatedAt this.updatedAt = dbContribution.updatedAt
this.updatedBy = dbContribution.updatedBy this.updatedBy = dbContribution.updatedBy
this.resubmissionAt = dbContribution.resubmissionAt this.resubmissionAt = dbContribution.resubmissionAt
if (ContributionStatus.CONFIRMED === dbContribution.contributionStatus) {
this.closedAt = dbContribution.confirmedAt
this.closedBy = dbContribution.confirmedBy
} else if (ContributionStatus.DELETED === dbContribution.contributionStatus) {
this.closedAt = dbContribution.deletedAt
this.closedBy = dbContribution.deletedBy
} else if (ContributionStatus.DENIED === dbContribution.contributionStatus) {
this.closedAt = dbContribution.deniedAt
this.closedBy = dbContribution.deniedBy
}
} }
@Field(() => Date, { nullable: true })
closedAt?: Date | null
@Field(() => Int, { nullable: true })
closedBy?: number | null
@Field(() => String, { nullable: true })
closedByUserName?: string | null
@Field(() => Date) @Field(() => Date)
createdAt: Date createdAt: Date
@Field(() => Int, { nullable: true })
moderatorId: number | null
@Field(() => String, { nullable: true })
moderatorUserName?: string | null
@Field(() => Date, { nullable: true }) @Field(() => Date, { nullable: true })
confirmedAt: Date | null confirmedAt: Date | null
@ -48,6 +74,9 @@ export class Contribution extends UnconfirmedContribution {
@Field(() => Int, { nullable: true }) @Field(() => Int, { nullable: true })
updatedBy: number | null updatedBy: number | null
@Field(() => String, { nullable: true })
updatedByUserName?: string | null
@Field(() => Date) @Field(() => Date)
contributionDate: Date contributionDate: Date

View File

@ -23,6 +23,7 @@ import {
Contribution as DbContribution, Contribution as DbContribution,
Transaction as DbTransaction, Transaction as DbTransaction,
User as DbUser, User as DbUser,
findUserNamesByIds,
getLastTransaction, getLastTransaction,
UserContact, UserContact,
} from 'database' } from 'database'
@ -348,6 +349,7 @@ export class ContributionResolver {
): Promise<ContributionListResult> { ): Promise<ContributionListResult> {
// Check if only count was requested (without contributionList) // Check if only count was requested (without contributionList)
const fields = Object.keys(extractGraphQLFields(info)) const fields = Object.keys(extractGraphQLFields(info))
// console.log(`fields: ${fields}`)
const countOnly: boolean = fields.includes('contributionCount') && fields.length === 1 const countOnly: boolean = fields.includes('contributionCount') && fields.length === 1
// check if related user was requested // check if related user was requested
const userRequested = const userRequested =
@ -370,8 +372,25 @@ export class ContributionResolver {
}, },
countOnly, countOnly,
) )
const result = new ContributionListResult(count, dbContributions)
return new ContributionListResult(count, dbContributions) const uniqueUserIds = new Set<number>()
const addIfExist = (userId?: number | null) => (userId ? uniqueUserIds.add(userId) : null)
for (const contribution of result.contributionList) {
addIfExist(contribution.updatedBy)
addIfExist(contribution.moderatorId)
addIfExist(contribution.closedBy)
}
const users = await findUserNamesByIds(Array.from(uniqueUserIds))
const getNameById = (userId?: number | null) => (userId ? (users.get(userId) ?? null) : null)
for (const contribution of result.contributionList) {
contribution.updatedByUserName = getNameById(contribution.updatedBy)
contribution.moderatorUserName = getNameById(contribution.moderatorId)
contribution.closedByUserName = getNameById(contribution.closedBy)
}
return result
} }
@Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION]) @Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION])

View File

@ -15,6 +15,8 @@ import {
EncryptedTransferArgs, EncryptedTransferArgs,
fullName, fullName,
interpretEncryptedTransferArgs, interpretEncryptedTransferArgs,
sendTransactionLinkRedeemedEmail,
sendTransactionReceivedEmail,
TransactionTypeId, TransactionTypeId,
} from 'core' } from 'core'
import { randomBytes } from 'crypto' import { randomBytes } from 'crypto'
@ -29,6 +31,7 @@ import {
User as DbUser, User as DbUser,
findModeratorCreatingContributionLink, findModeratorCreatingContributionLink,
findTransactionLinkByCode, findTransactionLinkByCode,
findUserByIdentifier,
getHomeCommunity, getHomeCommunity,
getLastTransaction, getLastTransaction,
} from 'database' } from 'database'
@ -567,7 +570,7 @@ export class TransactionLinkResolver {
} catch (e) { } catch (e) {
const errmsg = `Error on creating Redeem JWT: error=${e}` const errmsg = `Error on creating Redeem JWT: error=${e}`
methodLogger.error(errmsg) methodLogger.error(errmsg)
throw new LogError(errmsg) throw new Error(errmsg)
} }
} }
@ -609,19 +612,19 @@ export class TransactionLinkResolver {
if (!senderCom) { if (!senderCom) {
const errmsg = `Sender community not found with uuid=${senderCommunityUuid}` const errmsg = `Sender community not found with uuid=${senderCommunityUuid}`
methodLogger.error(errmsg) methodLogger.error(errmsg)
throw new LogError(errmsg) throw new Error(errmsg)
} }
const senderFedCom = await DbFederatedCommunity.findOneBy({ publicKey: senderCom.publicKey }) const senderFedCom = await DbFederatedCommunity.findOneBy({ publicKey: senderCom.publicKey })
if (!senderFedCom) { if (!senderFedCom) {
const errmsg = `Sender federated community not found with publicKey=${senderCom.publicKey}` const errmsg = `Sender federated community not found with publicKey=${senderCom.publicKey}`
methodLogger.error(errmsg) methodLogger.error(errmsg)
throw new LogError(errmsg) throw new Error(errmsg)
} }
const recipientCom = await getCommunityByUuid(recipientCommunityUuid) const recipientCom = await getCommunityByUuid(recipientCommunityUuid)
if (!recipientCom) { if (!recipientCom) {
const errmsg = `Recipient community not found with uuid=${recipientCommunityUuid}` const errmsg = `Recipient community not found with uuid=${recipientCommunityUuid}`
methodLogger.error(errmsg) methodLogger.error(errmsg)
throw new LogError(errmsg) throw new Error(errmsg)
} }
const client = DisbursementClientFactory.getInstance(senderFedCom) const client = DisbursementClientFactory.getInstance(senderFedCom)
if (client instanceof V1_0_DisbursementClient) { if (client instanceof V1_0_DisbursementClient) {
@ -660,6 +663,64 @@ export class TransactionLinkResolver {
if (methodLogger.isDebugEnabled()) { if (methodLogger.isDebugEnabled()) {
methodLogger.debug('Disburse JWT was sent successfully with result=', result) methodLogger.debug('Disburse JWT was sent successfully with result=', result)
} }
/* don't send email here, because it is sent by the sender community
const senderUser = await findUserByIdentifier(senderGradidoId, senderCommunityUuid)
if (!senderUser) {
const errmsg = `Sender user not found with identifier=${senderGradidoId}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
const recipientUser = await findUserByIdentifier(recipientGradidoId, recipientCommunityUuid)
if (!recipientUser) {
const errmsg = `Recipient user not found with identifier=${recipientGradidoId}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
if (recipientUser.emailContact?.email !== null) {
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(
'Sending TransactionLinkRedeem Email to recipient=' +
recipientUser.firstName +
' ' +
recipientUser.lastName +
'sender=' +
senderUser.firstName +
' ' +
senderUser.lastName,
)
}
try {
await sendTransactionLinkRedeemedEmail({
firstName: recipientUser.firstName,
lastName: recipientUser.lastName,
email: recipientUser.emailContact.email,
language: recipientUser.language,
senderFirstName: senderUser.firstName,
senderLastName: senderUser.lastName,
senderEmail: senderUser.emailContact?.email,
transactionMemo: memo,
transactionAmount: new Decimal(amount),
})
} catch (e) {
const errmsg = `Send TransactionLinkRedeem Email to recipient failed with error=${e}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
} else {
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(
'Sender or Recipient are foreign users with no email contact, not sending Transaction Received Email: recipient=' +
recipientUser.firstName +
' ' +
recipientUser.lastName +
'sender=' +
senderUser.firstName +
' ' +
senderUser.lastName,
)
}
}
*/
} catch (e) { } catch (e) {
const errmsg = `Disburse JWT was not sent successfully with error=${e}` const errmsg = `Disburse JWT was not sent successfully with error=${e}`
methodLogger.error(errmsg) methodLogger.error(errmsg)

View File

@ -214,6 +214,7 @@ export const executeTransaction = async (
transactionAmount: amount, transactionAmount: amount,
}) })
if (transactionLink) { if (transactionLink) {
const recipientCom = await getCommunityName(recipient.communityUuid)
await sendTransactionLinkRedeemedEmail({ await sendTransactionLinkRedeemedEmail({
firstName: sender.firstName, firstName: sender.firstName,
lastName: sender.lastName, lastName: sender.lastName,
@ -221,7 +222,7 @@ export const executeTransaction = async (
language: sender.language, language: sender.language,
senderFirstName: recipient.firstName, senderFirstName: recipient.firstName,
senderLastName: recipient.lastName, senderLastName: recipient.lastName,
senderEmail: recipient.emailContact.email, senderEmail: recipientCom, // recipient.emailContact.email,
transactionAmount: amount, transactionAmount: amount,
transactionMemo: memo, transactionMemo: memo,
}) })

View File

@ -66,6 +66,10 @@ export const findContributions = async (
if (relations?.user) { if (relations?.user) {
qb.orWhere('user.first_name LIKE :firstName', { firstName: queryString }) qb.orWhere('user.first_name LIKE :firstName', { firstName: queryString })
.orWhere('user.last_name LIKE :lastName', { lastName: queryString }) .orWhere('user.last_name LIKE :lastName', { lastName: queryString })
.orWhere('user.alias LIKE :alias', { alias: queryString })
.orWhere("LOWER(CONCAT(user.first_name, ' ', user.last_name)) LIKE LOWER(:fullName)", {
fullName: queryString.toLowerCase(),
})
.orWhere('emailContact.email LIKE :emailContact', { emailContact: queryString }) .orWhere('emailContact.email LIKE :emailContact', { emailContact: queryString })
.orWhere({ memo: Like(queryString) }) .orWhere({ memo: Like(queryString) })
} }

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "core", "name": "core",
"version": "2.7.3", "version": "2.7.4",
"description": "Gradido Core Code, High-Level Shared Code, with dependencies on other modules", "description": "Gradido Core Code, High-Level Shared Code, with dependencies on other modules",
"main": "./build/index.js", "main": "./build/index.js",
"types": "./src/index.ts", "types": "./src/index.ts",
@ -22,8 +22,8 @@
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"lint": "biome check --error-on-warnings .", "lint": "biome check --error-on-warnings .",
"lint:fix": "biome check --error-on-warnings . --write", "lint:fix": "biome check --error-on-warnings . --write",
"locales": "scripts/sort.sh src/locales", "locales": "bun scripts/sortLocales.ts",
"locales:fix": "scripts/sort.sh src/locales --fix", "locales:fix": "bun scripts/sortLocales.ts --fix",
"clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo" "clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo"
}, },
"dependencies": { "dependencies": {

View File

@ -0,0 +1,51 @@
#!/usr/bin/env bun
import { readdir, readFile, writeFile } from 'fs/promises'
import { join } from 'path'
const ROOT_DIR = join(import.meta.dir, '..')
const LOCALES_DIR = join(ROOT_DIR, 'src', 'locales')
const FIX = process.argv.includes('--fix')
function sortObject(value: any): any {
if (Array.isArray(value)) {
return value.map(sortObject)
}
if (value && typeof value === 'object') {
return Object.keys(value)
.sort()
.reduce<Record<string, any>>((acc, key) => {
acc[key] = sortObject(value[key])
return acc
}, {})
}
return value
}
let exitCode = 0
const files = (await readdir(LOCALES_DIR))
.filter(f => f.endsWith('.json'))
for (const file of files) {
const path = join(LOCALES_DIR, file)
const originalText = await readFile(path, 'utf8')
const originalJson = JSON.parse(originalText)
const sortedJson = sortObject(originalJson)
const sortedText = JSON.stringify(sortedJson, null, 2) + '\n'
if (originalText !== sortedText) {
if (FIX) {
await writeFile(path, sortedText)
} else {
console.error(`${file} is not sorted by keys`)
exitCode = 1
}
}
}
process.exit(exitCode)

View File

@ -0,0 +1,55 @@
import { getLogger } from 'log4js'
import { LOG4JS_BASE_CATEGORY_NAME } from '../config/const'
import { Command } from './Command'
const createLogger = (method: string) =>
getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.command.BaseCommand.${method}`)
export abstract class BaseCommand<T = any> implements Command<T> {
protected abstract requiredFields: string[]
protected constructor(protected readonly params: any[]) {
// this.validateRequiredFields();
}
abstract execute(): Promise<string | boolean | null | Error>
private validateRequiredFields(): void {
const methodLogger = createLogger(`validateRequiredFields`)
if (!this.requiredFields || this.requiredFields.length === 0) {
methodLogger.debug(`validateRequiredFields() no required fields`)
return
}
methodLogger.debug(
`validateRequiredFields() requiredFields=${JSON.stringify(this.requiredFields)}`,
)
/*
const commandArgs = JSON.parse(this.params[0])
const missingFields = this.requiredFields.filter(field =>
commandArgs.{ field } === undefined || commandArgs.{ field } === null || commandArgs.{ field } === ''
);
methodLogger.debug(`validateRequiredFields() missingFields=${JSON.stringify(missingFields)}`)
if (missingFields.length > 0) {
methodLogger.error(`validateRequiredFields() missing fields: ${missingFields.join(', ')}`)
throw new Error(`Missing required fields for ${this.constructor.name}: ${missingFields.join(', ')}`);
}
*/
}
validate(): boolean {
const methodLogger = createLogger(`validate`)
methodLogger.debug(
`validate() requiredFields=${JSON.stringify(this.requiredFields)} params=${JSON.stringify(this.params)}`,
)
/*
const isValid = this.requiredFields.every(field =>
this.params[field] !== undefined &&
this.params[field] !== null &&
this.params[field] !== ''
);
methodLogger.debug(`validate() isValid=${isValid}`)
*/
return true
}
}

View File

@ -0,0 +1,4 @@
export interface Command<_T = any> {
execute(): Promise<string | boolean | null | Error>
validate?(): boolean
}

View File

@ -0,0 +1,77 @@
// core/src/command/CommandExecutor.ts
import { getLogger } from 'log4js'
import { CommandJwtPayloadType } from 'shared'
import { LOG4JS_BASE_CATEGORY_NAME } from '../config/const'
import { interpretEncryptedTransferArgs } from '../graphql/logic/interpretEncryptedTransferArgs'
import { CommandResult } from '../graphql/model/CommandResult'
import { EncryptedTransferArgs } from '../graphql/model/EncryptedTransferArgs'
import { Command } from './Command'
import { CommandFactory } from './CommandFactory'
const createLogger = (method: string) =>
getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.command.CommandExecutor.${method}`)
export class CommandExecutor {
async executeCommand<T>(command: Command<T>): Promise<CommandResult> {
const methodLogger = createLogger(`executeCommand`)
methodLogger.debug(`executeCommand() command=${command.constructor.name}`)
try {
if (command.validate && !command.validate()) {
const errmsg = `Command validation failed for command=${command.constructor.name}`
methodLogger.error(errmsg)
return { success: false, error: errmsg }
}
methodLogger.debug(`executeCommand() executing command=${command.constructor.name}`)
const result = await command.execute()
methodLogger.debug(`executeCommand() executed result=${result}`)
return { success: true, data: result }
} catch (error) {
methodLogger.error(`executeCommand() error=${error}`)
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
}
}
}
async executeEncryptedCommand<_T>(encryptedArgs: EncryptedTransferArgs): Promise<CommandResult> {
const methodLogger = createLogger(`executeEncryptedCommand`)
try {
// Decrypt the command data
const commandArgs = (await interpretEncryptedTransferArgs(
encryptedArgs,
)) as CommandJwtPayloadType
if (!commandArgs) {
const errmsg = `invalid commandArgs payload of requesting community with publicKey=${encryptedArgs.publicKey}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(`executeEncryptedCommand() commandArgs=${JSON.stringify(commandArgs)}`)
}
const command = CommandFactory.getInstance().createCommand(
commandArgs.commandName,
commandArgs.commandArgs,
)
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(`executeEncryptedCommand() command=${JSON.stringify(command)}`)
}
// Execute the command
const result = await this.executeCommand(command)
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(`executeCommand() result=${JSON.stringify(result)}`)
}
return result
} catch (error) {
methodLogger.error(`executeEncryptedCommand() error=${error}`)
const errorResult: CommandResult = {
success: false,
error: error instanceof Error ? error.message : 'Failed to process command',
}
return errorResult
}
}
}

View File

@ -0,0 +1,81 @@
import { getLogger } from 'log4js'
import { LOG4JS_BASE_CATEGORY_NAME } from '../config/const'
import { BaseCommand } from './BaseCommand'
import { Command } from './Command'
import { ICommandConstructor } from './CommandTypes'
// import { ICommandConstructor } from './CommandTypes';
import { SendEmailCommand } from './commands/SendEmailCommand'
const createLogger = (method: string) =>
getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.command.CommandFactory.${method}`)
export class CommandFactory {
private static instance: CommandFactory
private commands: Map<string, ICommandConstructor> = new Map()
private constructor() {}
static getInstance(): CommandFactory {
if (!CommandFactory.instance) {
CommandFactory.instance = new CommandFactory()
}
return CommandFactory.instance
}
registerCommand<T>(name: string, commandClass: ICommandConstructor<T>): void {
const methodLogger = createLogger(`registerCommand`)
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(`registerCommand() name=${name}, commandClass=${commandClass.name}`)
}
this.commands.set(name, commandClass)
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(`registerCommand() commands=${JSON.stringify(this.commands.entries())}`)
}
}
createCommand<T>(name: string, params: string[]): Command<T> {
const methodLogger = createLogger(`createCommand`)
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(`createCommand() name=${name} params=${JSON.stringify(params)}`)
}
const CommandClass = this.commands.get(name)
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(
`createCommand() name=${name} commandClass=${CommandClass ? CommandClass.name : 'null'}`,
)
}
if (CommandClass === undefined) {
const errmsg = `Command ${name} not found`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
/*
try {
const command = new CommandClass(params) as Command<T>;
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(`createCommand() command=${JSON.stringify(command)}`)
}
return command;
} catch (error) {
const errmsg = `Failed to create command ${name}: ${error instanceof Error ? error.message : 'Unknown error'}`;
methodLogger.error(errmsg);
throw new Error(errmsg);
}
*/
let command: BaseCommand
switch (CommandClass.name) {
case 'SendEmailCommand':
command = new SendEmailCommand(params)
break
default: {
const errmsg = `Command ${name} not found`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
}
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(`createCommand() created command=${JSON.stringify(command)}`)
}
return command
}
}

View File

@ -0,0 +1,5 @@
import { Command } from './Command'
export interface ICommandConstructor<T = any> {
new (params: any): Command<T>
}

View File

@ -0,0 +1,145 @@
import { findUserByUuids } from 'database'
import Decimal from 'decimal.js-light'
import { getLogger } from 'log4js'
import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const'
import { sendTransactionReceivedEmail } from '../../emails/sendEmailVariants'
import { BaseCommand } from '../BaseCommand'
const createLogger = (method: string) =>
getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.command.commands.SendEmailCommand.${method}`)
export interface SendEmailCommandParams {
mailType: string
senderComUuid: string
senderGradidoId: string
receiverComUuid: string
receiverGradidoId: string
memo?: string
amount?: string
}
export class SendEmailCommand extends BaseCommand<
Record<string, unknown> | boolean | null | Error
> {
static readonly SEND_MAIL_COMMAND = 'SEND_MAIL_COMMAND'
protected requiredFields: string[] = [
'mailType',
'senderComUuid',
'senderGradidoId',
'receiverComUuid',
'receiverGradidoId',
]
protected sendEmailCommandParams: SendEmailCommandParams
constructor(params: any[]) {
const methodLogger = createLogger(`constructor`)
methodLogger.debug(`constructor() params=${JSON.stringify(params)}`)
super(params)
this.sendEmailCommandParams = JSON.parse(params[0]) as SendEmailCommandParams
}
validate(): boolean {
const baseValid = super.validate()
if (!baseValid) {
return false
}
// Additional validations
return true
}
async execute(): Promise<string | boolean | null | Error> {
const methodLogger = createLogger(`execute`)
methodLogger.debug(
`execute() sendEmailCommandParams=${JSON.stringify(this.sendEmailCommandParams)}`,
)
let result: string
if (!this.validate()) {
throw new Error('Invalid command parameters')
}
// find sender user
methodLogger.debug(
`find sender user: ${this.sendEmailCommandParams.senderComUuid} ${this.sendEmailCommandParams.senderGradidoId}`,
)
const senderUser = await findUserByUuids(
this.sendEmailCommandParams.senderComUuid,
this.sendEmailCommandParams.senderGradidoId,
true,
)
methodLogger.debug(`senderUser=${JSON.stringify(senderUser)}`)
if (!senderUser) {
const errmsg = `Sender user not found: ${this.sendEmailCommandParams.senderComUuid} ${this.sendEmailCommandParams.senderGradidoId}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
methodLogger.debug(
`find recipient user: ${this.sendEmailCommandParams.receiverComUuid} ${this.sendEmailCommandParams.receiverGradidoId}`,
)
const recipientUser = await findUserByUuids(
this.sendEmailCommandParams.receiverComUuid,
this.sendEmailCommandParams.receiverGradidoId,
)
methodLogger.debug(`recipientUser=${JSON.stringify(recipientUser)}`)
if (!recipientUser) {
const errmsg = `Recipient user not found: ${this.sendEmailCommandParams.receiverComUuid} ${this.sendEmailCommandParams.receiverGradidoId}`
methodLogger.error(errmsg)
throw new Error(errmsg)
}
const emailParams = {
firstName: recipientUser.firstName,
lastName: recipientUser.lastName,
email: recipientUser.emailContact.email,
language: recipientUser.language,
senderFirstName: senderUser.firstName,
senderLastName: senderUser.lastName,
senderEmail: senderUser.emailId !== null ? senderUser.emailContact.email : null,
memo: this.sendEmailCommandParams.memo || '',
transactionAmount: new Decimal(this.sendEmailCommandParams.amount || 0).abs(),
}
methodLogger.debug(`emailParams=${JSON.stringify(emailParams)}`)
switch (this.sendEmailCommandParams.mailType) {
case 'sendTransactionReceivedEmail': {
const emailResult = await sendTransactionReceivedEmail(emailParams)
result = this.getEmailResult(emailResult)
break
}
default:
throw new Error(`Unknown mail type: ${this.sendEmailCommandParams.mailType}`)
}
try {
// Example: const result = await emailService.sendEmail(this.params);
return result
} catch (error) {
methodLogger.error('Error executing SendEmailCommand:', error)
throw error
}
}
private getEmailResult(result: Record<string, unknown> | boolean | null | Error): string {
const methodLogger = createLogger(`getEmailResult`)
if (methodLogger.isDebugEnabled()) {
methodLogger.debug(`result=${JSON.stringify(result)}`)
}
let emailResult: string
if (result === null) {
emailResult = `result is null`
} else if (typeof result === 'boolean') {
emailResult = `result is ${result}`
} else if (result instanceof Error) {
emailResult = `error-message is ${result.message}`
} else if (typeof result === 'object') {
// {"accepted":["stage5@gradido.net"],"rejected":[],"ehlo":["PIPELINING","SIZE 25600000","ETRN","AUTH DIGEST-MD5 CRAM-MD5 PLAIN LOGIN","ENHANCEDSTATUSCODES","8BITMIME","DSN","CHUNKING"],"envelopeTime":23,"messageTime":135,"messageSize":37478,"response":"250 2.0.0 Ok: queued as C45C2100BD7","envelope":{"from":"stage5@gradido.net","to":["stage5@gradido.net"]},"messageId":"<d269161f-f3d2-2c96-49c0-58154366271b@gradido.net>"
const accepted = (result as Record<string, unknown>).accepted
const messageSize = (result as Record<string, unknown>).messageSize
const response = (result as Record<string, unknown>).response
const envelope = JSON.stringify((result as Record<string, unknown>).envelope)
emailResult = `accepted=${accepted}, messageSize=${messageSize}, response=${response}, envelope=${envelope}`
} else {
emailResult = `result is unknown type`
}
return emailResult
}
}

View File

@ -0,0 +1,11 @@
import { CommandFactory } from './CommandFactory'
import { SendEmailCommand } from './commands/SendEmailCommand'
// Import other commands...
export function initializeCommands(): void {
const factory = CommandFactory.getInstance()
// Register all commands
factory.registerCommand(SendEmailCommand.SEND_MAIL_COMMAND, SendEmailCommand)
// Register other commands...
}

View File

@ -77,34 +77,46 @@ export const sendEmailTranslated = async ({
...receiver, ...receiver,
attachments: [ attachments: [
{ {
// filename: 'gradido-header.jpeg', filename: 'gradido-header.jpeg',
content: gradidoHeader, content: gradidoHeader,
cid: 'gradidoheader', cid: 'gradidoheader',
contentType: 'image/jpeg',
contentDisposition: 'inline',
}, },
{ {
// filename: 'facebook-icon.png', filename: 'facebook-icon.png',
content: facebookIcon, content: facebookIcon,
cid: 'facebookicon', cid: 'facebookicon',
contentType: 'image/png',
contentDisposition: 'inline',
}, },
{ {
// filename: 'telegram-icon.png', filename: 'telegram-icon.png',
content: telegramIcon, content: telegramIcon,
cid: 'telegramicon', cid: 'telegramicon',
contentType: 'image/png',
contentDisposition: 'inline',
}, },
{ {
// filename: 'twitter-icon.png', filename: 'twitter-icon.png',
content: twitterIcon, content: twitterIcon,
cid: 'twittericon', cid: 'twittericon',
contentType: 'image/png',
contentDisposition: 'inline',
}, },
{ {
// filename: 'youtube-icon.png', filename: 'youtube-icon.png',
content: youtubeIcon, content: youtubeIcon,
cid: 'youtubeicon', cid: 'youtubeicon',
contentType: 'image/png',
contentDisposition: 'inline',
}, },
{ {
// filename: 'chatbox-icon.png', filename: 'chatbox-icon.png',
content: chatboxIcon, content: chatboxIcon,
cid: 'chatboxicon', cid: 'chatboxicon',
contentType: 'image/png',
contentDisposition: 'inline',
}, },
], ],
}, },

View File

@ -25,6 +25,7 @@ CONFIG.EMAIL_SENDER = 'info@gradido.net'
CONFIG.EMAIL_SMTP_HOST = testMailServerHost CONFIG.EMAIL_SMTP_HOST = testMailServerHost
CONFIG.EMAIL_SMTP_PORT = testMailServerPort CONFIG.EMAIL_SMTP_PORT = testMailServerPort
CONFIG.EMAIL_TLS = testMailTLS CONFIG.EMAIL_TLS = testMailTLS
CONFIG.EMAIL_TEST_MODUS = false
mock.module('nodemailer', () => { mock.module('nodemailer', () => {
return { return {

View File

@ -175,18 +175,18 @@ export const sendTransactionReceivedEmail = (
data: EmailCommonData & { data: EmailCommonData & {
senderFirstName: string senderFirstName: string
senderLastName: string senderLastName: string
senderEmail: string senderEmail: string | null
memo: string memo: string
transactionAmount: Decimal transactionAmount: Decimal
}, },
): Promise<Record<string, unknown> | boolean | null | Error> => { ): Promise<Record<string, unknown> | boolean | null | Error> => {
return sendEmailTranslated({ return sendEmailTranslated({
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
template: 'transactionReceived', template: data.senderEmail !== null ? 'transactionReceived' : 'transactionReceivedNoSender',
locals: { locals: {
...data, ...data,
transactionAmount: decimalSeparatorByLanguage(data.transactionAmount, data.language), transactionAmount: decimalSeparatorByLanguage(data.transactionAmount, data.language),
...getEmailCommonLocales(), ...(data.senderEmail !== null ? getEmailCommonLocales() : { locale: data.language }),
}, },
}) })
} }

View File

@ -0,0 +1,24 @@
extend ../layout.pug
block content
//
mixin mailto(email, subject)
- var formattedSubject = encodeURIComponent(subject)
a(class!=attributes.class href=`mailto:${email}?subject=${formattedSubject}`)
block
- var subject= t('emails.transactionReceived.replySubject', { senderFirstName, senderLastName, transactionAmount })
h2= t('emails.transactionReceived.title', { senderFirstName, senderLastName, transactionAmount })
.text-block
include ../includes/salutation.pug
p
= t('emails.transactionReceived.haveReceivedAmountGDDFrom', { transactionAmount, senderFirstName, senderLastName })
.content
h2= t('emails.general.message')
.child-left
div(class="p_content")= memo
a.button-3(href=`${communityURL}/transactions`) #{t('emails.general.toAccount')}

View File

@ -0,0 +1 @@
= t('emails.transactionReceived.subject', { senderFirstName, senderLastName, transactionAmount })

View File

@ -0,0 +1,45 @@
import { FederatedCommunity as DbFederatedCommunity } from 'database'
import { GraphQLClient } from 'graphql-request'
import { getLogger } from 'log4js'
import { LOG4JS_BASE_CATEGORY_NAME } from '../../../config/const'
import { EncryptedTransferArgs } from '../../../graphql/model/EncryptedTransferArgs'
import { ensureUrlEndsWithSlash } from '../../../util/utilities'
import { sendCommand as sendCommandQuery } from './query/sendCommand'
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.client.1_0.CommandClient`)
export class CommandClient {
dbCom: DbFederatedCommunity
endpoint: string
client: GraphQLClient
constructor(dbCom: DbFederatedCommunity) {
this.dbCom = dbCom
this.endpoint = ensureUrlEndsWithSlash(dbCom.endPoint).concat(dbCom.apiVersion).concat('/')
this.client = new GraphQLClient(this.endpoint, {
method: 'POST',
jsonSerializer: {
parse: JSON.parse,
stringify: JSON.stringify,
},
})
}
async sendCommand(args: EncryptedTransferArgs): Promise<boolean> {
logger.debug(`sendCommand at ${this.endpoint} for args:`, args)
try {
const { data } = await this.client.rawRequest<{ success: boolean }>(sendCommandQuery, {
args,
})
if (!data?.success) {
logger.warn('sendCommand without response data from endpoint', this.endpoint)
return false
}
logger.debug('sendCommand successfully started with endpoint', this.endpoint)
return true
} catch (err) {
logger.error('error on sendCommand: ', err)
return false
}
}
}

View File

@ -0,0 +1,11 @@
import { gql } from 'graphql-request'
export const sendCommand = gql`
mutation ($args: EncryptedTransferArgs!) {
sendCommand(encryptedArgs: $args) {
success
data
error
}
}
`

View File

@ -0,0 +1,3 @@
import { CommandClient as V1_0_CommandClient } from '../1_0/CommandClient'
export class CommandClient extends V1_0_CommandClient {}

View File

@ -0,0 +1,55 @@
import { ApiVersionType } from 'core'
import { FederatedCommunity as DbFederatedCommunity } from 'database'
import { CommandClient as V1_0_CommandClient } from './1_0/CommandClient'
import { CommandClient as V1_1_CommandClient } from './1_1/CommandClient'
type CommandClient = V1_0_CommandClient | V1_1_CommandClient
interface CommandClientInstance {
id: number
client: CommandClient
}
export class CommandClientFactory {
private static instanceArray: CommandClientInstance[] = []
/**
* The Singleton's constructor should always be private to prevent direct
* construction calls with the `new` operator.
*/
private constructor() {}
private static createCommandClient = (dbCom: DbFederatedCommunity) => {
switch (dbCom.apiVersion) {
case ApiVersionType.V1_0:
return new V1_0_CommandClient(dbCom)
case ApiVersionType.V1_1:
return new V1_1_CommandClient(dbCom)
default:
return null
}
}
/**
* The static method that controls the access to the singleton instance.
*
* This implementation let you subclass the Singleton class while keeping
* just one instance of each subclass around.
*/
public static getInstance(dbCom: DbFederatedCommunity): CommandClient | null {
const instance = CommandClientFactory.instanceArray.find((instance) => instance.id === dbCom.id)
if (instance) {
return instance.client
}
const client = CommandClientFactory.createCommandClient(dbCom)
if (client) {
CommandClientFactory.instanceArray.push({
id: dbCom.id,
client,
} as CommandClientInstance)
}
return client
}
}

View File

@ -0,0 +1,14 @@
import { getLogger } from 'log4js'
import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const'
const createLogger = (method: string) =>
getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.logic.processCommand.${method}`)
export async function processCommand(
commandClass: string,
commandMethod: string,
commandArgs: string[],
) {
const methodLogger = createLogger(`processCommand`)
methodLogger.info('processing a command...')
}

View File

@ -16,6 +16,7 @@ import { Decimal } from 'decimal.js-light'
// import { LogError } from '@server/LogError' // import { LogError } from '@server/LogError'
import { getLogger } from 'log4js' import { getLogger } from 'log4js'
import { import {
CommandJwtPayloadType,
encryptAndSign, encryptAndSign,
PendingTransactionState, PendingTransactionState,
SendCoinsJwtPayloadType, SendCoinsJwtPayloadType,
@ -23,11 +24,15 @@ import {
verifyAndDecrypt, verifyAndDecrypt,
} from 'shared' } from 'shared'
import { randombytes_random } from 'sodium-native' import { randombytes_random } from 'sodium-native'
import { SendEmailCommand } from '../../command/commands/SendEmailCommand'
import { CONFIG as CONFIG_CORE } from '../../config' import { CONFIG as CONFIG_CORE } from '../../config'
import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const' import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const'
import { sendTransactionLinkRedeemedEmail, sendTransactionReceivedEmail } from '../../emails'
import { CommandClient as V1_0_CommandClient } from '../../federation/client/1_0/CommandClient'
import { SendCoinsResultLoggingView } from '../../federation/client/1_0/logging/SendCoinsResultLogging.view' import { SendCoinsResultLoggingView } from '../../federation/client/1_0/logging/SendCoinsResultLogging.view'
import { SendCoinsResult } from '../../federation/client/1_0/model/SendCoinsResult' import { SendCoinsResult } from '../../federation/client/1_0/model/SendCoinsResult'
import { SendCoinsClient as V1_0_SendCoinsClient } from '../../federation/client/1_0/SendCoinsClient' import { SendCoinsClient as V1_0_SendCoinsClient } from '../../federation/client/1_0/SendCoinsClient'
import { CommandClientFactory } from '../../federation/client/CommandClientFactory'
import { SendCoinsClientFactory } from '../../federation/client/SendCoinsClientFactory' import { SendCoinsClientFactory } from '../../federation/client/SendCoinsClientFactory'
import { TransactionTypeId } from '../../graphql/enum/TransactionTypeId' import { TransactionTypeId } from '../../graphql/enum/TransactionTypeId'
import { EncryptedTransferArgs } from '../../graphql/model/EncryptedTransferArgs' import { EncryptedTransferArgs } from '../../graphql/model/EncryptedTransferArgs'
@ -167,6 +172,19 @@ export async function processXComCompleteTransaction(
) )
} }
} }
if (dbTransactionLink) {
await sendTransactionLinkRedeemedEmail({
firstName: senderUser.firstName,
lastName: senderUser.lastName,
email: senderUser.emailContact.email,
language: senderUser.language,
senderFirstName: foreignUser.firstName,
senderLastName: foreignUser.lastName,
senderEmail: recipientCom.name!, // foreignUser.emailContact.email,
transactionAmount: new Decimal(amount),
transactionMemo: memo,
})
}
} }
} catch (err) { } catch (err) {
const errmsg = const errmsg =
@ -227,7 +245,7 @@ export async function processXComPendingSendCoins(
const receiverFCom = await DbFederatedCommunity.findOneOrFail({ const receiverFCom = await DbFederatedCommunity.findOneOrFail({
where: { where: {
publicKey: Buffer.from(receiverCom.publicKey), publicKey: receiverCom.publicKey,
apiVersion: CONFIG_CORE.FEDERATION_BACKEND_SEND_ON_API, apiVersion: CONFIG_CORE.FEDERATION_BACKEND_SEND_ON_API,
}, },
}) })
@ -484,6 +502,37 @@ export async function processXComCommittingSendCoins(
sendCoinsResult.recipGradidoID = pendingTx.linkedUserGradidoID sendCoinsResult.recipGradidoID = pendingTx.linkedUserGradidoID
sendCoinsResult.recipAlias = recipient.recipAlias sendCoinsResult.recipAlias = recipient.recipAlias
} }
// ** after successfull settle of the pending transaction on sender side we have to send a trigger to the recipient community to send an email to the x-com-tx recipient
const cmdClient = CommandClientFactory.getInstance(receiverFCom)
if (cmdClient instanceof V1_0_CommandClient) {
const payload = new CommandJwtPayloadType(
handshakeID,
SendEmailCommand.SEND_MAIL_COMMAND,
SendEmailCommand.name,
[
JSON.stringify({
mailType: 'sendTransactionReceivedEmail',
senderComUuid: senderCom.communityUuid,
senderGradidoId: sender.gradidoID,
receiverComUuid: receiverCom.communityUuid,
receiverGradidoId: sendCoinsResult.recipGradidoID,
memo: pendingTx.memo,
amount: pendingTx.amount,
}),
],
)
const jws = await encryptAndSign(
payload,
senderCom.privateJwtKey!,
receiverCom.publicJwtKey!,
)
const args = new EncryptedTransferArgs()
args.publicKey = senderCom.publicKey.toString('hex')
args.jwt = jws
args.handshakeID = handshakeID
cmdClient.sendCommand(args)
}
} catch (err) { } catch (err) {
methodLogger.error( methodLogger.error(
`Error in writing sender pending transaction: ${JSON.stringify(err, null, 2)}`, `Error in writing sender pending transaction: ${JSON.stringify(err, null, 2)}`,

View File

@ -1,5 +1,13 @@
import { Community as DbCommunity, User as DbUser, findForeignUserByUuids } from 'database' import {
Community as DbCommunity,
User as DbUser,
UserContact as DbUserContact,
findForeignUserByUuids,
UserContactLoggingView,
UserLoggingView,
} from 'database'
import { getLogger } from 'log4js' import { getLogger } from 'log4js'
import { UserContactType } from 'shared'
import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const' import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const'
import { SendCoinsResult } from '../../federation/client/1_0/model/SendCoinsResult' import { SendCoinsResult } from '../../federation/client/1_0/model/SendCoinsResult'
@ -35,17 +43,38 @@ export async function storeForeignUser(
} }
foreignUser.gradidoID = committingResult.recipGradidoID foreignUser.gradidoID = committingResult.recipGradidoID
foreignUser = await DbUser.save(foreignUser) foreignUser = await DbUser.save(foreignUser)
logger.debug('new foreignUser inserted:', foreignUser)
logger.debug('new foreignUser inserted:', new UserLoggingView(foreignUser))
/*
if (committingResult.recipEmail !== null) {
let foreignUserEmail = DbUserContact.create()
foreignUserEmail.email = committingResult.recipEmail!
foreignUserEmail.emailChecked = true
foreignUserEmail.user = foreignUser
foreignUserEmail = await DbUserContact.save(foreignUserEmail)
logger.debug(
'new foreignUserEmail inserted:',
new UserContactLoggingView(foreignUserEmail),
)
foreignUser.emailContact = foreignUserEmail
foreignUser.emailId = foreignUserEmail.id
foreignUser = await DbUser.save(foreignUser)
}
*/
return foreignUser return foreignUser
} else if ( } else if (
user.firstName !== committingResult.recipFirstName || user.firstName !== committingResult.recipFirstName ||
user.lastName !== committingResult.recipLastName || user.lastName !== committingResult.recipLastName ||
user.alias !== committingResult.recipAlias user.alias !== committingResult.recipAlias /* ||
(user.emailContact === null && committingResult.recipEmail !== null) ||
(user.emailContact !== null &&
user.emailContact?.email !== null &&
user.emailContact?.email !== committingResult.recipEmail)
*/
) { ) {
logger.warn( logger.debug(
'foreignUser still exists, but with different name or alias:', 'foreignUser still exists, but with different name or alias:',
user, new UserLoggingView(user),
committingResult, committingResult,
) )
if (committingResult.recipFirstName !== null) { if (committingResult.recipFirstName !== null) {
@ -57,11 +86,39 @@ export async function storeForeignUser(
if (committingResult.recipAlias !== null) { if (committingResult.recipAlias !== null) {
user.alias = committingResult.recipAlias user.alias = committingResult.recipAlias
} }
/*
if (!user.emailContact && committingResult.recipEmail !== null) {
logger.debug(
'creating new userContact:',
new UserContactLoggingView(user.emailContact),
committingResult,
)
let foreignUserEmail = DbUserContact.create()
foreignUserEmail.type = UserContactType.USER_CONTACT_EMAIL
foreignUserEmail.email = committingResult.recipEmail!
foreignUserEmail.emailChecked = true
foreignUserEmail.user = user
foreignUserEmail.userId = user.id
foreignUserEmail = await DbUserContact.save(foreignUserEmail)
logger.debug(
'new foreignUserEmail inserted:',
new UserContactLoggingView(foreignUserEmail),
)
user.emailContact = foreignUserEmail
user.emailId = foreignUserEmail.id
} else if (user.emailContact && committingResult.recipEmail != null) {
const userContact = user.emailContact
userContact.email = committingResult.recipEmail
user.emailContact = await DbUserContact.save(userContact)
user.emailId = userContact.id
logger.debug('foreignUserEmail updated:', new UserContactLoggingView(userContact))
}
*/
await DbUser.save(user) await DbUser.save(user)
logger.debug('update recipient successful.', user) logger.debug('update recipient successful.', new UserLoggingView(user))
return user return user
} else { } else {
logger.debug('foreignUser still exists...:', user) logger.debug('foreignUser still exists...:', new UserLoggingView(user))
return user return user
} }
} catch (err) { } catch (err) {

View File

@ -0,0 +1,13 @@
import { Field, ObjectType } from 'type-graphql'
@ObjectType()
export class CommandResult {
@Field(() => Boolean)
success: boolean
@Field(() => String, { nullable: true })
data?: any
@Field(() => String, { nullable: true })
error?: string
}

View File

@ -1,5 +1,9 @@
export * from './command/CommandExecutor'
export * from './command/CommandFactory'
export * from './command/initCommands'
export * from './config/index' export * from './config/index'
export * from './emails' export * from './emails'
export { CommandClient as V1_0_CommandClient } from './federation/client/1_0/CommandClient'
export * from './federation/client/1_0/logging/SendCoinsArgsLogging.view' export * from './federation/client/1_0/logging/SendCoinsArgsLogging.view'
export * from './federation/client/1_0/logging/SendCoinsResultLogging.view' export * from './federation/client/1_0/logging/SendCoinsResultLogging.view'
export * from './federation/client/1_0/model/SendCoinsArgs' export * from './federation/client/1_0/model/SendCoinsArgs'
@ -9,15 +13,19 @@ export * from './federation/client/1_0/query/revertSettledSendCoins'
export * from './federation/client/1_0/query/settleSendCoins' export * from './federation/client/1_0/query/settleSendCoins'
export * from './federation/client/1_0/query/voteForSendCoins' export * from './federation/client/1_0/query/voteForSendCoins'
export { SendCoinsClient as V1_0_SendCoinsClient } from './federation/client/1_0/SendCoinsClient' export { SendCoinsClient as V1_0_SendCoinsClient } from './federation/client/1_0/SendCoinsClient'
export { CommandClient as V1_1_CommandClient } from './federation/client/1_1/CommandClient'
export { SendCoinsClient as V1_1_SendCoinsClient } from './federation/client/1_1/SendCoinsClient' export { SendCoinsClient as V1_1_SendCoinsClient } from './federation/client/1_1/SendCoinsClient'
export * from './federation/client/CommandClientFactory'
export * from './federation/client/SendCoinsClientFactory' export * from './federation/client/SendCoinsClientFactory'
export * from './federation/enum/apiVersionType' export * from './federation/enum/apiVersionType'
export * from './graphql/enum/TransactionTypeId' export * from './graphql/enum/TransactionTypeId'
export * from './graphql/logging/DecayLogging.view' export * from './graphql/logging/DecayLogging.view'
export * from './graphql/logic/interpretEncryptedTransferArgs' export * from './graphql/logic/interpretEncryptedTransferArgs'
export * from './graphql/logic/processCommand'
export * from './graphql/logic/processXComSendCoins' export * from './graphql/logic/processXComSendCoins'
export * from './graphql/logic/settlePendingSenderTransaction' export * from './graphql/logic/settlePendingSenderTransaction'
export * from './graphql/logic/storeForeignUser' export * from './graphql/logic/storeForeignUser'
export * from './graphql/model/CommandResult'
export * from './graphql/model/Decay' export * from './graphql/model/Decay'
export * from './graphql/model/EncryptedTransferArgs' export * from './graphql/model/EncryptedTransferArgs'
export * from './logic' export * from './logic'

View File

@ -18,10 +18,10 @@
}, },
"addedContributionMessage": { "addedContributionMessage": {
"commonGoodContributionMessage": "du hast zu deinem Gemeinwohl-Beitrag „{contributionMemo}“ eine Nachricht von {senderFirstName} {senderLastName} erhalten.", "commonGoodContributionMessage": "du hast zu deinem Gemeinwohl-Beitrag „{contributionMemo}“ eine Nachricht von {senderFirstName} {senderLastName} erhalten.",
"message": "„{message}“",
"readMessage": "Nachricht lesen und beantworten", "readMessage": "Nachricht lesen und beantworten",
"subject": "Nachricht zu deinem Gemeinwohl-Beitrag", "subject": "Nachricht zu deinem Gemeinwohl-Beitrag",
"title": "Nachricht zu deinem Gemeinwohl-Beitrag", "title": "Nachricht zu deinem Gemeinwohl-Beitrag",
"message": "„{message}“",
"toSeeAndAnswerMessage": "Um auf die Nachricht zu antworten, gehe in deinem Gradido-Konto ins Menü „Schöpfen“ auf den Tab „Meine Beiträge“." "toSeeAndAnswerMessage": "Um auf die Nachricht zu antworten, gehe in deinem Gradido-Konto ins Menü „Schöpfen“ auf den Tab „Meine Beiträge“."
}, },
"contribution": { "contribution": {
@ -90,8 +90,8 @@
}, },
"transactionReceived": { "transactionReceived": {
"haveReceivedAmountGDDFrom": "du hast soeben {transactionAmount} GDD erhalten von {senderFirstName} {senderLastName}", "haveReceivedAmountGDDFrom": "du hast soeben {transactionAmount} GDD erhalten von {senderFirstName} {senderLastName}",
"subject": "{senderFirstName} {senderLastName} hat dir {transactionAmount} Gradido gesendet",
"replySubject": "Re: {senderFirstName} {senderLastName} hat dir {transactionAmount} Gradido gesendet", "replySubject": "Re: {senderFirstName} {senderLastName} hat dir {transactionAmount} Gradido gesendet",
"subject": "{senderFirstName} {senderLastName} hat dir {transactionAmount} Gradido gesendet",
"title": "{senderFirstName} {senderLastName} hat dir {transactionAmount} Gradido gesendet" "title": "{senderFirstName} {senderLastName} hat dir {transactionAmount} Gradido gesendet"
} }
}, },

View File

@ -18,10 +18,10 @@
}, },
"addedContributionMessage": { "addedContributionMessage": {
"commonGoodContributionMessage": "You have received a message from {senderFirstName} {senderLastName} regarding your common good contribution “{contributionMemo}”.", "commonGoodContributionMessage": "You have received a message from {senderFirstName} {senderLastName} regarding your common good contribution “{contributionMemo}”.",
"message": "„{message}“",
"readMessage": "Read and reply to message", "readMessage": "Read and reply to message",
"subject": "Message about your common good contribution", "subject": "Message about your common good contribution",
"title": "Message about your common good contribution", "title": "Message about your common good contribution",
"message": "„{message}“",
"toSeeAndAnswerMessage": "To reply to the message, go to the “Creation” menu in your Gradido account and click on the “My contributions” tab." "toSeeAndAnswerMessage": "To reply to the message, go to the “Creation” menu in your Gradido account and click on the “My contributions” tab."
}, },
"contribution": { "contribution": {

View File

@ -1,5 +1,3 @@
CONFIG_VERSION=$DATABASE_CONFIG_VERSION
DB_HOST=127.0.0.1 DB_HOST=127.0.0.1
DB_PORT=3306 DB_PORT=3306
DB_USER=$DB_USER DB_USER=$DB_USER

View File

@ -25,7 +25,7 @@ const run = async (command: string) => {
host: ${CONFIG.DB_HOST} host: ${CONFIG.DB_HOST}
port: ${CONFIG.DB_PORT} port: ${CONFIG.DB_PORT}
user: ${CONFIG.DB_USER} user: ${CONFIG.DB_USER}
password: ${CONFIG.DB_PASSWORD.slice(-2)} password: last 2 character: ${CONFIG.DB_PASSWORD.slice(-2)}
database: ${CONFIG.DB_DATABASE}`, database: ${CONFIG.DB_DATABASE}`,
) )
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "database", "name": "database",
"version": "2.7.3", "version": "2.7.4",
"description": "Gradido Database Tool to execute database migrations", "description": "Gradido Database Tool to execute database migrations",
"main": "./build/index.js", "main": "./build/index.js",
"types": "./src/index.ts", "types": "./src/index.ts",

View File

@ -1,7 +1,7 @@
import { drizzle, MySql2Database } from 'drizzle-orm/mysql2' import { drizzle, MySql2Database } from 'drizzle-orm/mysql2'
import Redis from 'ioredis' import Redis from 'ioredis'
import { getLogger } from 'log4js' import { getLogger } from 'log4js'
import { Connection, createConnection } from 'mysql2/promise' import { Connection, createConnection, createPool, Pool } from 'mysql2/promise'
import { DataSource as DBDataSource, FileLogger } from 'typeorm' import { DataSource as DBDataSource, FileLogger } from 'typeorm'
import { latestDbVersion } from '.' import { latestDbVersion } from '.'
import { CONFIG } from './config' import { CONFIG } from './config'
@ -14,7 +14,7 @@ export class AppDatabase {
private static instance: AppDatabase private static instance: AppDatabase
private dataSource: DBDataSource | undefined private dataSource: DBDataSource | undefined
private drizzleDataSource: MySql2Database | undefined private drizzleDataSource: MySql2Database | undefined
private drizzleConnection: Connection | undefined private drizzlePool: Pool | undefined
private redisClient: Redis | undefined private redisClient: Redis | undefined
/** /**
@ -48,10 +48,10 @@ export class AppDatabase {
} }
public getDrizzleDataSource(): MySql2Database { public getDrizzleDataSource(): MySql2Database {
if (!this.drizzleDataSource) { if (!this.drizzlePool) {
throw new Error('Drizzle connection not initialized') throw new Error('Drizzle connection pool not initialized')
} }
return this.drizzleDataSource return drizzle({ client: this.drizzlePool })
} }
// create database connection, initialize with automatic retry and check for correct database version // create database connection, initialize with automatic retry and check for correct database version
@ -106,22 +106,25 @@ export class AppDatabase {
logger.info('Redis status=', this.redisClient.status) logger.info('Redis status=', this.redisClient.status)
if (!this.drizzleDataSource) { if (!this.drizzleDataSource) {
this.drizzleConnection = await createConnection({ this.drizzlePool = createPool({
host: CONFIG.DB_HOST, host: CONFIG.DB_HOST,
user: CONFIG.DB_USER, user: CONFIG.DB_USER,
password: CONFIG.DB_PASSWORD, password: CONFIG.DB_PASSWORD,
database: CONFIG.DB_DATABASE, database: CONFIG.DB_DATABASE,
port: CONFIG.DB_PORT, port: CONFIG.DB_PORT,
waitForConnections: true,
connectionLimit: 20,
queueLimit: 100,
enableKeepAlive: true,
keepAliveInitialDelay: 10000,
}) })
this.drizzleDataSource = drizzle({ client: this.drizzleConnection })
} }
} }
public async destroy(): Promise<void> { public async destroy(): Promise<void> {
await Promise.all([this.dataSource?.destroy(), this.drizzleConnection?.end()]) await Promise.all([this.dataSource?.destroy(), this.drizzlePool?.end()])
this.dataSource = undefined this.dataSource = undefined
this.drizzleConnection = undefined this.drizzlePool = undefined
this.drizzleDataSource = undefined
if (this.redisClient) { if (this.redisClient) {
await this.redisClient.quit() await this.redisClient.quit()
this.redisClient = undefined this.redisClient = undefined

View File

@ -17,21 +17,25 @@ export class UserContactLoggingView extends AbstractLoggingView {
public toJSON(): any { public toJSON(): any {
return { return {
id: this.self.id, self: this.self
type: this.self.type, ? {
user: id: this.self.id,
this.showUser && this.self.user type: this.self.type,
? new UserLoggingView(this.self.user).toJSON() user:
: { id: this.self.userId }, this.showUser && this.self.user
email: this.self.email?.substring(0, 3) + '...', ? new UserLoggingView(this.self.user).toJSON()
emailVerificationCode: this.self.emailVerificationCode?.substring(0, 4) + '...', : { id: this.self.userId },
emailOptInTypeId: OptInType[this.self.emailOptInTypeId], email: this.self.email?.substring(0, 3) + '...',
emailResendCount: this.self.emailResendCount, emailVerificationCode: this.self.emailVerificationCode?.substring(0, 4) + '...',
emailChecked: this.self.emailChecked, emailOptInTypeId: OptInType[this.self.emailOptInTypeId],
phone: this.self.phone ? this.self.phone.substring(0, 3) + '...' : undefined, emailResendCount: this.self.emailResendCount,
createdAt: this.dateToString(this.self.createdAt), emailChecked: this.self.emailChecked,
updatedAt: this.dateToString(this.self.updatedAt), phone: this.self.phone ? this.self.phone.substring(0, 3) + '...' : undefined,
deletedAt: this.dateToString(this.self.deletedAt), createdAt: this.dateToString(this.self.createdAt),
updatedAt: this.dateToString(this.self.updatedAt),
deletedAt: this.dateToString(this.self.deletedAt),
}
: undefined,
} }
} }
} }

View File

@ -23,7 +23,7 @@ afterAll(async () => {
describe('openaiThreads query test', () => { describe('openaiThreads query test', () => {
it('should insert a new openai thread', async () => { it('should insert a new openai thread', async () => {
await Promise.resolve([dbInsertOpenaiThread('7', 1), dbInsertOpenaiThread('72', 6)]) await Promise.all([dbInsertOpenaiThread('7', 1), dbInsertOpenaiThread('72', 6)])
const result = await db.select().from(openaiThreadsTable) const result = await db.select().from(openaiThreadsTable)
expect(result).toHaveLength(2) expect(result).toHaveLength(2)
expect(result).toMatchObject([ expect(result).toMatchObject([

View File

@ -1,6 +1,6 @@
import { getLogger } from 'log4js' import { getLogger } from 'log4js'
import { aliasSchema, emailSchema, uuidv4Schema } from 'shared' import { aliasSchema, emailSchema, uuidv4Schema } from 'shared'
import { Raw } from 'typeorm' import { In, Raw } from 'typeorm'
import { User as DbUser, UserContact as DbUserContact } from '../entity' import { User as DbUser, UserContact as DbUserContact } from '../entity'
import { findWithCommunityIdentifier, LOG4JS_QUERIES_CATEGORY_NAME } from './index' import { findWithCommunityIdentifier, LOG4JS_QUERIES_CATEGORY_NAME } from './index'
@ -81,3 +81,26 @@ export async function findForeignUserByUuids(
where: { foreign: true, communityUuid, gradidoID }, where: { foreign: true, communityUuid, gradidoID },
}) })
} }
export async function findUserByUuids(
communityUuid: string,
gradidoID: string,
foreign: boolean = false,
): Promise<DbUser | null> {
return DbUser.findOne({
where: { foreign, communityUuid, gradidoID },
relations: ['emailContact'],
})
}
export async function findUserNamesByIds(userIds: number[]): Promise<Map<number, string>> {
const users = await DbUser.find({
select: { id: true, firstName: true, lastName: true, alias: true },
where: { id: In(userIds) },
})
return new Map(
users.map((user) => {
return [user.id, `${user.firstName} ${user.lastName}`]
}),
)
}

View File

@ -1,7 +1,13 @@
# Need to adjust! # Need to adjust!
# Will be seen on login and from other communities if dht and federation modules are active
COMMUNITY_NAME="Your community name" COMMUNITY_NAME="Your community name"
# Description should have at least 10 characters
COMMUNITY_DESCRIPTION="Short Description from your Community." COMMUNITY_DESCRIPTION="Short Description from your Community."
# your domain name, without protocol (without https:// or http:// )
# domain name should be configured in your dns records to point to this server
# hetzner_cloud/install.sh will be acquire a SSL-certificate via letsencrypt for this domain
COMMUNITY_HOST=gddhost.tld COMMUNITY_HOST=gddhost.tld
# used in E-Mails and some error message, should be the email address of your own support (team)
COMMUNITY_SUPPORT_MAIL=support@supportmail.com COMMUNITY_SUPPORT_MAIL=support@supportmail.com
# setup email account for sending gradido system messages to users # setup email account for sending gradido system messages to users
@ -12,10 +18,44 @@ EMAIL_PASSWORD=1234
EMAIL_SMTP_HOST=smtp.lustig.de EMAIL_SMTP_HOST=smtp.lustig.de
EMAIL_SMTP_PORT=587 EMAIL_SMTP_PORT=587
BACKEND_PORT=4000 # Federation, Settings for transactions with other communities
# if set to true allow sending gradidos to another communities # if set to true allow sending gradidos to another communities
FEDERATION_XCOM_SENDCOINS_ENABLED=false FEDERATION_XCOM_SENDCOINS_ENABLED=false
# if set to true, allow redeeming gradido link from another community
CROSS_TX_REDEEM_LINK_ACTIVE=false
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
# on an hash created from this topic
# need for discover other communities
# get the correct topic from Gradido Academy, GRADIDO_HUB is our test topic
FEDERATION_DHT_TOPIC=GRADIDO_HUB
# Advanced Server Setup
# SCSS Parsing
# grass speed up frontend building, but needs some time for the first setup because it is a rust program which need to be compiled
USE_GRASS=false
# Logging
LOG_LEVEL=info
GRADIDO_LOG_PATH=/home/gradido/gradido/deployment/bare_metal/log
TYPEORM_LOGGING_RELATIVE_PATH=../deployment/bare_metal/log/typeorm.backend.log
# Need adjustments for test system
# protocol for community host, on production usually https, on local dev usually http
URL_PROTOCOL=https
# only for test server
DEPLOY_SEED_DATA=false
# test email
# if true all email will be send to EMAIL_TEST_RECEIVER instead of email address of user
EMAIL_TEST_MODUS=false
EMAIL_TEST_RECEIVER=test_team@gradido.net
# webhook for auto update on github repository changes
WEBHOOK_GITHUB_BRANCH=master
# Business Logic
# how many minutes email verification code is valid # how many minutes email verification code is valid
# also used for password reset code # also used for password reset code
@ -23,36 +63,8 @@ EMAIL_CODE_VALID_TIME=1440
# how many minutes user must wait before he can request the email verification code again # how many minutes user must wait before he can request the email verification code again
# also used for password reset code # also used for password reset code
EMAIL_CODE_REQUEST_TIME=10 EMAIL_CODE_REQUEST_TIME=10
# login expire time
# Need to adjust by updates JWT_EXPIRES_IN=10m
# config versions
DATABASE_CONFIG_VERSION=v1.2022-03-18
BACKEND_CONFIG_VERSION=v23.2024-04-04
FRONTEND_CONFIG_VERSION=v6.2024-02-27
ADMIN_CONFIG_VERSION=v2.2024-01-04
FEDERATION_CONFIG_VERSION=v2.2023-08-24
FEDERATION_DHT_CONFIG_VERSION=v4.2024-01-17
FEDERATION_DHT_TOPIC=GRADIDO_HUB
# Need adjustments for test system
URL_PROTOCOL=https
# start script
# only for test server
DEPLOY_SEED_DATA=false
# test email
# if true all email will be send to EMAIL_TEST_RECEIVER instead of email address of user
EMAIL_TEST_MODUS=false
EMAIL_TEST_RECEIVER=test_team@gradido.net
USE_CRYPTO_WORKER=true
# Logging
LOG_LEVEL=info
GRADIDO_LOG_PATH=/home/gradido/gradido/deployment/bare_metal/log
TYPEORM_LOGGING_RELATIVE_PATH=../deployment/bare_metal/log/typeorm.backend.log
# webhook
WEBHOOK_GITHUB_SECRET=secret
WEBHOOK_GITHUB_BRANCH=master
# frontend and admin paths, usually don't need changes # frontend and admin paths, usually don't need changes
# used in nginx config and for links in emails # used in nginx config and for links in emails
@ -66,44 +78,19 @@ EMAIL_LINK_SETPASSWORD_PATH=/reset-password/
EMAIL_LINK_FORGOTPASSWORD_PATH=/forgot-password EMAIL_LINK_FORGOTPASSWORD_PATH=/forgot-password
EMAIL_LINK_OVERVIEW_PATH=/overview EMAIL_LINK_OVERVIEW_PATH=/overview
ADMIN_AUTH_PATH=/admin/authenticate?token= ADMIN_AUTH_PATH=/admin/authenticate?token=
# need to change when frontend is hosted on a different domain as the backend
WALLET_URL=$URL_PROTOCOL://$COMMUNITY_HOST
GRAPHQL_PATH=/graphql GRAPHQL_PATH=/graphql
# login expire time
JWT_SECRET=secret123
JWT_EXPIRES_IN=10m
# Federation # Federation
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
# on an hash created from this topic
# FEDERATION_DHT_TOPIC=GRADIDO_HUB # FEDERATION_DHT_TOPIC=GRADIDO_HUB
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f #
# the api port is the baseport, which will be added with the api-version, e.g. 1_0 = 5010 # the api port is the baseport, which will be added with the api-version, e.g. 1_0 = 5010
FEDERATION_COMMUNITY_API_PORT=5000 FEDERATION_COMMUNITY_API_PORT=5000
FEDERATION_VALIDATE_COMMUNITY_TIMER=60000
# comma separated list of api-versions, which cause starting several federation modules # comma separated list of api-versions, which cause starting several federation modules
FEDERATION_COMMUNITY_APIS=1_0 FEDERATION_COMMUNITY_APIS=1_0
# externe gradido services (more added in future)
GDT_ACTIVE=false
AUTO_POLL_INTERVAL=30000
# DLT-Connector (still in develop)
DLT_ACTIVE=false
DLT_CONNECTOR_PORT=6010
DLT_NODE_SERVER_PORT=8340
DLT_NODE_SERVER_URL=$URL_PROTOCOL://$COMMUNITY_HOST/dlt
DLT_GRADIDO_NODE_SERVER_HOME_FOLDER=/home/gradido/.gradido
# used for combining a newsletter on klicktipp with this gradido community
# if used, user will be subscribed on register and can unsubscribe in his account
KLICKTIPP=false
KLICKTIPP_USER=gradido_test
KLICKTIPP_PASSWORD=secret321
KLICKTIPP_APIKEY_DE=SomeFakeKeyDE
KLICKTIPP_APIKEY_EN=SomeFakeKeyEN
# Meta data in frontend pages, important when shared via facebook or twitter or for search engines # Meta data in frontend pages, important when shared via facebook or twitter or for search engines
META_TITLE_DE="Gradido Dein Dankbarkeitskonto" META_TITLE_DE="Gradido Dein Dankbarkeitskonto"
META_TITLE_EN="Gradido - Your gratitude account" META_TITLE_EN="Gradido - Your gratitude account"
@ -113,6 +100,35 @@ META_KEYWORDS_DE="Grundeinkommen, Währung, Dankbarkeit, Schenk-Ökonomie, Natü
META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System" META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System"
META_AUTHOR="Bernd Hückstädt - Gradido-Akademie" META_AUTHOR="Bernd Hückstädt - Gradido-Akademie"
# externe gradido services
GDT_ACTIVE=false
GDT_API_URL=https://gdt.gradido.net
# DLT-Connector (still in develop)
# verify transaction additional via gradido blockchain
DLT_ACTIVE=false
DLT_CONNECTOR_PORT=6010
DLT_NODE_SERVER_PORT=8340
DLT_GRADIDO_NODE_SERVER_HOME_FOLDER=/home/gradido/.gradido
# HIERO used from dlt-connector
HIERO_HEDERA_NETWORK=testnet
# https://docs.hedera.com/hedera/networks/testnet/testnet-access
HIERO_OPERATOR_ID=
HIERO_OPERATOR_KEY=
# for inspector from outside of server
DLT_NODE_SERVER_URL=$URL_PROTOCOL://$COMMUNITY_HOST/dlt
# used for combining a newsletter on klicktipp with this gradido community
# if used, user will be subscribed on register and can unsubscribe in his account
KLICKTIPP=false
KLICKTIPP_USER=gradido_test
KLICKTIPP_PASSWORD=secret321
KLICKTIPP_APIKEY_DE=SomeFakeKeyDE
KLICKTIPP_APIKEY_EN=SomeFakeKeyEN
# update page shown while updating gradido # update page shown while updating gradido
# page will be fed with status changes # page will be fed with status changes
NGINX_UPDATE_PAGE_ROOT=/home/gradido/gradido/deployment/bare_metal/nginx/update-page NGINX_UPDATE_PAGE_ROOT=/home/gradido/gradido/deployment/bare_metal/nginx/update-page
@ -124,24 +140,82 @@ NGINX_SSL_DHPARAM=/etc/letsencrypt/ssl-dhparams.pem
NGINX_SSL_INCLUDE=/etc/letsencrypt/options-ssl-nginx.conf NGINX_SSL_INCLUDE=/etc/letsencrypt/options-ssl-nginx.conf
NGINX_REWRITE_LEGACY_URLS=false NGINX_REWRITE_LEGACY_URLS=false
# LEGACY # Services
WEBHOOK_ELOPAGE_SECRET=secret BACKEND_PORT=4000
# GMS # GMS
# Speak with Gradido Academy to get access to GMS
GMS_ACTIVE=false GMS_ACTIVE=false
# Coordinates of Illuminz test instance GMS_API_URL=https://gms-stage.gradido.net/gms
#GMS_API_URL=http://54.176.169.179:3071 GMS_DASHBOARD_URL=https://gms-stage.gradido.net/
GMS_API_URL=http://localhost:4044/ GMS_USER_SEARCH_FRONTEND_ROUTE=user-search
GMS_DASHBOARD_URL=http://localhost:8080/ # set your own or placeholder webhook_secret will be replaced by start.sh
GMS_WEBHOOK_SECRET=secret GMS_WEBHOOK_SECRET=webhook_secret
GMS_CREATE_USER_THROW_ERRORS=false GMS_CREATE_USER_THROW_ERRORS=false
# HUMHUB # HUMHUB
# Host your own humhub Server
HUMHUB_ACTIVE=false HUMHUB_ACTIVE=false
HUMHUB_API_URL=https://community.gradido.net HUMHUB_API_URL=https://your-humhub-domain.tld
HUMHUB_JWT_KEY= # Humhub jwt key, setup together with humhub
# set your own or placeholder jwt_secret will be replaced by start.sh
HUMHUB_JWT_KEY=jwt_secret
# OPENAI # OPENAI
# you need a paid openai account to use this feature
OPENAI_ACTIVE=false OPENAI_ACTIVE=false
OPENAI_API_KEY='' # assistant id for your openai assistant
OPENAI_ASSISTANT_ID=asst_MF5cchZr7wY7rNXayuWvZFsM OPENAI_ASSISTANT_ID=
# your api key form openai
OPENAI_API_KEY=
# Performance Settings
# in milliseconds, how often inspector and frontend ask on some views for new data
AUTO_POLL_INTERVAL=60000
# set to true if password encryption should take place in another thread, which don't block the main loop, true is recommended
USE_CRYPTO_WORKER=true
# in milliseconds, how often the other communities should be checked if still online and reachable
FEDERATION_VALIDATE_COMMUNITY_TIMER=60000
# Security Settings, placeholder (jwt_secret, webhook_secret, binary8_secret, binary16_secret, binary32_secret)
# will be replaced by start.sh
JWT_SECRET=jwt_secret
# secret for webhook, must be set here and in github.com webhook
WEBHOOK_GITHUB_SECRET=webhook_secret
# basic for key pair generation for federation, maximal 64 characters
FEDERATION_DHT_SEED=binary32_secret
WEBHOOK_ELOPAGE_SECRET=webhook_secret
# keys for dlt
GRADIDO_BLOCKCHAIN_CRYPTO_APP_SECRET=binary8_secret
GRADIDO_BLOCKCHAIN_SERVER_CRYPTO_KEY=binary16_secret
HOME_COMMUNITY_SEED=binary32_secret
# Server Settings needed if modules are hosted on different servers (non-default setup)
# GUI-Module Configs (with nginx-routing always running on COMMUNITY_HOST)
ADMIN_MODULE_PROTOCOL=$URL_PROTOCOL
ADMIN_MODULE_HOST=$COMMUNITY_HOST
ADMIN_MODULE_PORT=8080
FRONTEND_MODULE_PROTOCOL=$URL_PROTOCOL
FRONTEND_MODULE_HOST=$COMMUNITY_HOST
FRONTEND_MODULE_PORT=3000
# Non-GUI-Module Configs (with nginx-routing always running on localhost in case of on bare metal)
BACKEND_MODULE_PROTOCOL=http
BACKEND_MODULE_HOST=localhost
BACKEND_PORT=4000
DHT_MODULE_PROTOCOL=http
DHT_MODULE_HOST=localhost
DHT_MODULE_PORT=5000
DLT_MODULE_PROTOCOL=http
DLT_MODULE_HOST=localhost
DLT_MODULE_PORT=6010
FEDERATION_MODULE_PROTOCOL=http
FEDERATION_MODULE_HOST=localhost
FEDERATION_MODULE_PORT=5010

View File

@ -40,6 +40,8 @@ ENV BRANCH_NAME=$BRANCH_NAME
RUN git clone https://github.com/gradido/gradido.git --branch $BRANCH_NAME RUN git clone https://github.com/gradido/gradido.git --branch $BRANCH_NAME
RUN cp /app/gradido/deployment/bare_metal/.env.dist /app/gradido/deployment/bare_metal/.env RUN cp /app/gradido/deployment/bare_metal/.env.dist /app/gradido/deployment/bare_metal/.env
RUN sed -i 's/^URL_PROTOCOL=https$/URL_PROTOCOL=http/' /app/gradido/deployment/bare_metal/.env RUN sed -i 's/^URL_PROTOCOL=https$/URL_PROTOCOL=http/' /app/gradido/deployment/bare_metal/.env
RUN sed -i 's/^COMMUNITY_HOST=gddhost.tld$/COMMUNITY_HOST=localhost/' /app/gradido/deployment/bare_metal/.env
# setup nginx # setup nginx
WORKDIR /app/gradido/deployment/bare_metal/nginx WORKDIR /app/gradido/deployment/bare_metal/nginx

View File

@ -1,10 +1,11 @@
#!/bin/bash #!/bin/bash
# source profile so PATH/NVM/BUN werden gesetzt (safe for non-login)
if [ -f /home/gradido/.profile ]; then
. /home/gradido/.profile
fi
# Ensure required tools are installed # Ensure required tools are installed
# make sure correct node version is installed
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
if ! command -v nvm > /dev/null if ! command -v nvm > /dev/null
then then
echo "'nvm' is missing, will be installed now!" echo "'nvm' is missing, will be installed now!"
@ -14,7 +15,7 @@ install_nvm() {
nvm install nvm install
nvm use nvm use
nvm alias default nvm alias default
npm i -g yarn pm2 npm i -g pm2 turbo
pm2 startup pm2 startup
} }
nvm use || install_nvm nvm use || install_nvm
@ -52,23 +53,24 @@ fi
if ! command -v turbo > /dev/null if ! command -v turbo > /dev/null
then then
echo "'turbo' is missing, will be installed now!" echo "'turbo' is missing, will be installed now!"
bun install --global turbo npm i -g turbo
fi fi
# rust and grass # rust and grass
if ! command -v cargo > /dev/null if [ "$USE_GRASS" = true ]; then
then if ! command -v cargo > /dev/null
echo "'cargo' is missing, will be installed now!" then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y echo "'cargo' is missing, will be installed now!"
export CARGO_HOME="$HOME/.cargo" curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
export PATH="$CARGO_HOME/bin:$PATH" export CARGO_HOME="$HOME/.cargo"
export PATH="$CARGO_HOME/bin:$PATH"
fi
if ! command -v grass > /dev/null
then
echo "'grass' is missing, will be installed now!"
cargo install grass
fi
fi fi
if ! command -v grass > /dev/null
then
echo "'grass' is missing, will be installed now!"
cargo install grass
fi
# redis # redis
if ! command -v redis-cli --version > /dev/null if ! command -v redis-cli --version > /dev/null
then then

View File

@ -1,149 +0,0 @@
#!/bin/bash
# This install script requires the minimum requirements already installed.
# How to do this is described in detail in [setup.md](./setup.md)
# Find current directory & configure paths
## For manualy use in terminal
## set -o allexport
## SCRIPT_DIR=$(pwd)
## PROJECT_ROOT=$SCRIPT_DIR/../..
## set +o allexport
# Use here in script
set -o allexport
SCRIPT_PATH=$(realpath $0)
SCRIPT_DIR=$(dirname $SCRIPT_PATH)
PROJECT_ROOT=$SCRIPT_DIR/../..
set +o allexport
# Load .env or .env.dist if not present
# NOTE: all config values will be in process.env when starting
# the services and will therefore take precedence over the .env
if [ -f "$SCRIPT_DIR/.env" ]; then
set -o allexport
source $SCRIPT_DIR/.env
set +o allexport
else
set -o allexport
source $SCRIPT_DIR/.env.dist
set +o allexport
fi
# Configure git
git config pull.ff only
# Install mariadb
sudo apt-get install -y mariadb-server
sudo mysql_secure_installation
# Enter current password for root (enter for none): enter
# Switch to unix_socket authentication [Y/n] Y
# Change the root password? [Y/n] n
# Remove anonymous users? [Y/n] Y
# Disallow root login remotely? [Y/n] Y
# Remove test database and access to it? [Y/n] Y
# Reload privilege tables now? [Y/n] Y
# Install nginx
sudo apt-get install -y nginx
sudo rm /etc/nginx/sites-enabled/default
sudo ln -s /home/gradido/gradido/deployment/bare_metal/nginx/sites-available/gradido.conf /etc/nginx/sites-available
# sudo ln -s /etc/nginx/sites-available/gradido.conf /etc/nginx/sites-enabled
sudo ln -s /home/gradido/gradido/deployment/bare_metal/nginx/sites-available/update-page.conf /etc/nginx/sites-available
sudo ln -s /home/gradido/gradido/deployment/bare_metal/nginx/common /etc/nginx/
sudo rmdir /etc/nginx/conf.d
sudo ln -s /home/gradido/gradido/deployment/bare_metal/nginx/conf.d /etc/nginx/
# Allow nginx configuration and restart for gradido
#TODO generate file
sudo nano /etc/sudoers.d/gradido
> gradido ALL=(ALL) NOPASSWD: /etc/init.d/nginx start,/etc/init.d/nginx stop,/etc/init.d/nginx restart
sudo chmod a+rw /etc/nginx/sites-enabled
# Install node 16.x
sudo apt-get install -y curl
curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt-get install -y nodejs
sudo apt-get install -y build-essential
# Install yarn
sudo apt-get install -y gnupg
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update
sudo apt-get install -y yarn
# Install pm2
sudo yarn global add pm2
pm2 startup
> execute command output in shell
# Install certbot
sudo apt-get install -y certbot
sudo apt-get install -y python3-certbot-nginx
sudo certbot
> Enter email address (used for urgent renewal and security notices) > e.g. support@supportmail.com
> Please read the Terms of Service at > Y
> Would you be willing, once your first certificate is successfully issued, to > N
> No names were found in your configuration files. Please enter in your domain > stage1.gradido.net
# Note: this will throw an error regarding not beeing able to identify the nginx corresponding
# config but produce the required certificate - thats perfectly fine this way
# Troubleshoot: to manually renew a certificate with running nginx use the following command:
# (this might be required once to properly have things setup for the cron to autorenew)
# sudo certbot --nginx -d example.com -d www.example.com
# Troubleshoot: to check ut if things working you can use
# sudo certbot renew --dry-run
# Install logrotate
sudo apt-get install -y logrotate
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $SCRIPT_DIR/logrotate/gradido.conf.template > $SCRIPT_DIR/logrotate/gradido.conf
sudo cp $SCRIPT_DIR/logrotate/gradido.conf.template /etc/logrotate.d/gradido.conf
sudo chown root:root /etc/logrotate.d/gradido.conf
# Install mysql autobackup
sudo apt-get install -y automysqlbackup
# Webhooks (optional) (for development)
sudo apt install -y webhook
# TODO generate
# put hook into github
# TODO adjust secret
# TODO adjust branch if needed
# https://stage1.gradido.net/hooks/github
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $SCRIPT_DIR/webhook/hooks.json.template > ~/hooks.json
webhook -hooks ~/hooks.json &
# or for debugging
# webhook -hooks ~/hooks.json -verbose
# create db user
export DB_USER=gradido
export DB_PASSWORD=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-32};echo);
sudo mysql <<EOFMYSQL
CREATE USER '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASSWORD';
GRANT ALL PRIVILEGES ON *.* TO '$DB_USER'@'localhost';
FLUSH PRIVILEGES;
EOFMYSQL
# Configure database
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/database/.env.template > $PROJECT_ROOT/database/.env
# Configure backend
export JWT_SECRET=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-32};echo);
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/backend/.env.template > $PROJECT_ROOT/backend/.env
# Configure frontend
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/frontend/.env.template > $PROJECT_ROOT/frontend/.env
# Configure admin
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/admin/.env.template > $PROJECT_ROOT/admin/.env
# Configure dht-node
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/dht-node/.env.template > $PROJECT_ROOT/dht-node/.env
# create cronjob to delete yarn output in /tmp
# crontab -e
# hourly job: 0 * * * * find /tmp -name "yarn--*" -cmin +60 -exec rm -r {} \; > /dev/null
# daily job: 0 4 * * * find /tmp -name "yarn--*" -ctime +1 -exec rm -r {} \; > /dev/null
# Start gradido
# Note: on first startup some errors will occur - nothing serious
./start.sh

View File

@ -2,6 +2,11 @@
# stop if something fails # stop if something fails
set -euo pipefail set -euo pipefail
# source profile so PATH/NVM/BUN werden gesetzt (safe for non-login)
if [ -f /home/gradido/.profile ]; then
. /home/gradido/.profile
fi
# check for parameter # check for parameter
FAST_MODE=false FAST_MODE=false
POSITIONAL_ARGS=() POSITIONAL_ARGS=()
@ -184,7 +189,7 @@ cd $PROJECT_ROOT
# TODO: this overfetches alot, but ensures we can use start.sh with tags # TODO: this overfetches alot, but ensures we can use start.sh with tags
git fetch --all git fetch --all
git checkout $BRANCH_NAME git checkout $BRANCH_NAME
git pull git pull origin $BRANCH_NAME
git submodule update --init --recursive git submodule update --init --recursive
export BUILD_COMMIT="$(git rev-parse HEAD)" export BUILD_COMMIT="$(git rev-parse HEAD)"
@ -299,7 +304,7 @@ done
# Install all node_modules # Install all node_modules
log_step 'Installing node_modules' log_step 'Installing node_modules'
bun install bun install --frozen-lockfile
# build all modules # build all modules
log_step 'build all modules' log_step 'build all modules'
@ -309,12 +314,12 @@ turbo build --env-mode=loose --concurrency=$(nproc)
if [ "$DLT_ACTIVE" = true ]; then if [ "$DLT_ACTIVE" = true ]; then
log_step 'build inspector' log_step 'build inspector'
cd $PROJECT_ROOT/inspector cd $PROJECT_ROOT/inspector
bun install bun install --frozen-lockfile
bun run build bun run build
log_step 'build dlt-connector' log_step 'build dlt-connector'
cd $PROJECT_ROOT/dlt-connector cd $PROJECT_ROOT/dlt-connector
bun install bun install --frozen-lockfile
bun run build bun run build
cd $PROJECT_ROOT cd $PROJECT_ROOT

View File

@ -1,6 +1,15 @@
# Migration # Migration
[Migration from 2.2.0 to 2.2.1](migration/2_2_0-2_2_1/README.md) [Migration from 2.2.0 to 2.2.1](migration/2_2_0-2_2_1/README.md)
# Key Pair
It is recommended to create a new ssh key pair for your gradido server.
You can create it with this command:
```bash
ssh-keygen -t ed25519 -C "your_email@example.com"
```
**Reason**: We recommend `ed25519` because it provides strong security with smaller key sizes, faster performance, and resistance to known attacks, making it more secure and efficient than traditional RSA keys.
# Setup on Hetzner Cloud Server # Setup on Hetzner Cloud Server
Suggested OS: Suggested OS:
Debian 12 Debian 12
@ -15,7 +24,7 @@ ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAkLGbzbG7KIGfkssKJBkc/0EVAzQ/8vjvVHzNdxhK8J yourname - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAkLGbzbG7KIGfkssKJBkc/0EVAzQ/8vjvVHzNdxhK8J yourname
``` ```
I made a (german) video to show it to you: I made a (german) video to show it to you (video is older, cloudConfig.yaml differ):
[![Video](https://img.youtube.com/vi/fORK3Bt3lPw/hqdefault.jpg)](https://www.youtube.com/watch?v=fORK3Bt3lPw) [![Video](https://img.youtube.com/vi/fORK3Bt3lPw/hqdefault.jpg)](https://www.youtube.com/watch?v=fORK3Bt3lPw)
@ -23,14 +32,7 @@ I made a (german) video to show it to you:
### setup your domain pointing on server ip address ### setup your domain pointing on server ip address
### login to your new server as root ### login to your new server as root
```bash ```bash
ssh -i /path/to/privKey root@gddhost.tld ssh -i ~/.ssh/id_ed25519 root@gddhost.tld
```
### Change default shell
```bash
chsh -s /bin/bash
chsh -s /bin/bash gradido
``` ```
### Set password for user `gradido` ### Set password for user `gradido`
@ -51,16 +53,15 @@ su gradido
If you logout from the server you can test authentication: If you logout from the server you can test authentication:
```bash ```bash
$ ssh -i /path/to/privKey gradido@gddhost.tld $ ssh -i ~/.ssh/id_ed25519 gradido@gddhost.tld
# This should log you in and allow you to use sudo commands, which will require the user's password # This should log you in and allow you to use sudo commands, which will require the user's password
``` ```
### Disable password root login via ssh ### Disable password root login via ssh
```bash ```bash
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.org sudo sed -i -e '/^\(#\|\)PermitRootLogin/s/^.*$/PermitRootLogin no/' /etc/ssh/sshd_config.d/ssh-hardening.conf
sudo sed -i -e '/^\(#\|\)PermitRootLogin/s/^.*$/PermitRootLogin no/' /etc/ssh/sshd_config sudo sed -i '$a AllowUsers gradido' /etc/ssh/sshd_config.d/ssh-hardening.conf
sudo sed -i '$a AllowUsers gradido' /etc/ssh/sshd_config
sudo /etc/init.d/ssh restart sudo /etc/init.d/ssh restart
``` ```
@ -69,16 +70,17 @@ sudo /etc/init.d/ssh restart
```bash ```bash
$ ssh gradido@gddhost.tld $ ssh gradido@gddhost.tld
# Will result in in either a passphrase request for your key or the message 'Permission denied (publickey)' # Will result in in either a passphrase request for your key or the message 'Permission denied (publickey)'
$ ssh -i /path/to/privKey root@gddhost.tld $ ssh -i ~/.ssh/id_ed25519 root@gddhost.tld
# Will result in 'Permission denied (publickey)' # Will result in 'Permission denied (publickey)'
$ ssh -i /path/to/privKey gradido@gddhost.tld $ ssh -i ~/.ssh/id_ed25519 gradido@gddhost.tld
# Will succeed after entering the correct keys passphrase (if any) # Will succeed after entering the correct keys passphrase (if any)
``` ```
### Install `Gradido` code ### Install `Gradido` code
`latest` is a tag pointing on last stable release
```bash ```bash
cd ~ cd ~
git clone https://github.com/gradido/gradido.git git clone https://github.com/gradido/gradido.git --branch latest --depth 1
``` ```
### Adjust the values in `.env` ### Adjust the values in `.env`
@ -99,21 +101,35 @@ All your following installations in `install.sh` will fail!*
cd ~/gradido/deployment/bare_metal cd ~/gradido/deployment/bare_metal
cp .env.dist .env cp .env.dist .env
nano .env nano .env
# adjust values accordingly
``` ```
### Run `install.sh` with branch name For a minimal setup you need at least to change this values:
```env
COMMUNITY_NAME="Your community name"
COMMUNITY_DESCRIPTION="Short Description from your Community."
# your domain name, without protocol (without https:// or http:// )
# domain name should be configured in your dns records to point to this server
# hetzner_cloud/install.sh will be acquire a SSL-certificate via letsencrypt for this domain
COMMUNITY_HOST=gddhost.tld
# setup email account for sending gradido system messages to users
EMAIL_USERNAME=peter@lustig.de
EMAIL_SENDER=peter@lustig.de
EMAIL_PASSWORD=1234
EMAIL_SMTP_HOST=smtp.lustig.de
```
### Run `install.sh` with branch or tag name
***!!! Attention !!!*** ***!!! Attention !!!***
Don't use this script if you have custom config in /etc/nginx/conf.d, because this script Don't use this script if you have custom config in /etc/nginx/conf.d, because this script
will remove it and ln ../bare_metal/nginx/conf.d will remove it and ln ../bare_metal/nginx/conf.d
```bash ```bash
cd ~/gradido/deployment/hetzner_cloud cd ~/gradido/deployment/hetzner_cloud
sudo ./install.sh release-2_2_0 sudo ./install.sh latest
``` ```
I made a (german) video to show it to you: I made a (german) video to show it to you (video is older, output will differ):
[![Video](https://img.youtube.com/vi/9h-55Si6bMk/hqdefault.jpg)](https://www.youtube.com/watch?v=9h-55Si6bMk) [![Video](https://img.youtube.com/vi/9h-55Si6bMk/hqdefault.jpg)](https://www.youtube.com/watch?v=9h-55Si6bMk)
@ -133,3 +149,16 @@ sudo mysql -D gradido_community -e "insert into user_roles(user_id, role) values
I made a (german) video to show it to you: I made a (german) video to show it to you:
[![Video](https://img.youtube.com/vi/xVQ5t4MnLrE/hqdefault.jpg)](https://www.youtube.com/watch?v=xVQ5t4MnLrE) [![Video](https://img.youtube.com/vi/xVQ5t4MnLrE/hqdefault.jpg)](https://www.youtube.com/watch?v=xVQ5t4MnLrE)
### Troubleshooting
If after some tests this error occur, right after `Requesting a certificate for your-domain.tld`, try again another day. Letsencrypt is rate limited:
```bash
An unexpected error occurred:
AttributeError: can't set attribute
```
### But it isn't working
If it isn't working you can write us: [support@gradido.net](mailto:support@gradido.net)

View File

@ -14,6 +14,7 @@ packages:
- git - git
- mariadb-server - mariadb-server
- nginx - nginx
- redis
- curl - curl
- build-essential - build-essential
- gnupg - gnupg
@ -22,6 +23,7 @@ packages:
- logrotate - logrotate
- automysqlbackup - automysqlbackup
- expect - expect
- unzip
package_update: true package_update: true
package_upgrade: true package_upgrade: true
write_files: write_files:

View File

@ -1,7 +1,28 @@
#!/bin/bash #!/bin/bash
# stop if something fails
set -euo pipefail
log_error() {
local message="$1"
echo -e "\e[31m$message\e[0m" # red in console
}
# called always on error, log error really visible with ascii art in red on console and html
# stop script execution
onError() {
local exit_code=$?
log_error "Command failed!"
log_error " /\\_/\\ Line: $(caller 0)"
log_error "( x.x ) Exit Code: $exit_code"
log_error " > < Offending command: '$BASH_COMMAND'"
log_error ""
exit 1
}
trap onError ERR
# check for parameter # check for parameter
if [ -z "$1" ]; then if [ -z "$1" ]; then
echo "Usage: Please provide a branch name as the first argument." log_error "Usage: Please provide a branch name as the first argument."
exit 1 exit 1
fi fi
@ -19,25 +40,88 @@ LOCAL_SCRIPT_DIR=$(dirname $LOCAL_SCRIPT_PATH)
PROJECT_ROOT=$SCRIPT_DIR/.. PROJECT_ROOT=$SCRIPT_DIR/..
set +o allexport set +o allexport
# Replace placeholder secrets in .env
echo 'Replace placeholder secrets in .env'
# Load .env or .env.dist if not present
# NOTE: all config values will be in process.env when starting
# the services and will therefore take precedence over the .env
if [ -f "$SCRIPT_PATH/.env" ]; then
ENV_FILE="$SCRIPT_PATH/.env"
# --- Secret Generators -------------------------------------------------------
gen_jwt_secret() {
# 32 Character, URL-safe: A-Z a-z 0-9 _ -
tr -dc 'A-Za-z0-9_-' < /dev/urandom | head -c 32 2>/dev/null || true
}
gen_webhook_secret() {
# URL-safe, longer security (40 chars)
tr -dc 'A-Za-z0-9_-' < /dev/urandom | head -c 40 2>/dev/null || true
}
gen_binary_secret() {
local bytes="$1"
# Hex -> 2 chars pro byte
openssl rand -hex "$bytes" 2>/dev/null || true
}
# --- Mapping of Placeholder -> Function --------------------------------------
generate_secret_for() {
case "$1" in
jwt_secret) gen_jwt_secret ;;
webhook_secret) gen_webhook_secret ;;
binary8_secret) gen_binary_secret 8 ;;
binary16_secret) gen_binary_secret 16;;
binary32_secret) gen_binary_secret 32;;
*)
echo "Unknown Placeholder: $1" >&2
exit 1
;;
esac
}
# --- Placeholder List --------------------------------------------------------
placeholders=(
"jwt_secret"
"webhook_secret"
"binary8_secret"
"binary16_secret"
"binary32_secret"
)
# --- Processing in .env -------------------------------------------------
TMP_FILE="${ENV_FILE}.tmp"
cp "$ENV_FILE" "$TMP_FILE"
for ph in "${placeholders[@]}"; do
# Iterate over all lines containing the placeholder
while grep -q "$ph" "$TMP_FILE"; do
new_value=$(generate_secret_for "$ph")
# Replace only the first occurrence per line
sed -i "0,/$ph/s//$new_value/" "$TMP_FILE"
done
done
# Write back
mv "$TMP_FILE" "$ENV_FILE"
chown gradido:gradido "$ENV_FILE"
fi
# If install.sh will be called more than once # If install.sh will be called more than once
# We have to load the backend .env to get DB_USERNAME, DB_PASSWORD AND JWT_SECRET # We have to load the backend .env to get DB_USERNAME and DB_PASSWORD
# and the dht-node .env to get FEDERATION_DHT_SEED
export_var(){ export_var(){
export $1=$(grep -v '^#' $PROJECT_ROOT/backend/.env | grep -e "$1" | sed -e 's/.*=//') export $1=$(grep -v '^#' $PROJECT_ROOT/backend/.env | grep -e "$1" | sed -e 's/.*=//')
export $1=$(grep -v '^#' $PROJECT_ROOT/dht-node/.env | grep -e "$1" | sed -e 's/.*=//')
} }
if [ -f "$PROJECT_ROOT/backend/.env" ]; then if [ -f "$PROJECT_ROOT/backend/.env" ]; then
export_var 'DB_USER' export_var 'DB_USER'
export_var 'DB_PASSWORD' export_var 'DB_PASSWORD'
export_var 'JWT_SECRET'
fi fi
if [ -f "$PROJECT_ROOT/dht-node/.env" ]; then
export_var 'FEDERATION_DHT_SEED'
fi
# Load .env or .env.dist if not present # Load .env or .env.dist if not present
# NOTE: all config values will be in process.env when starting # NOTE: all config values will be in process.env when starting
# the services and will therefore take precedence over the .env # the services and will therefore take precedence over the .env
@ -97,15 +181,17 @@ systemctl restart fail2ban
rm /etc/nginx/sites-enabled/default rm /etc/nginx/sites-enabled/default
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $SCRIPT_PATH/nginx/sites-available/gradido.conf.template > $SCRIPT_PATH/nginx/sites-available/gradido.conf envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $SCRIPT_PATH/nginx/sites-available/gradido.conf.template > $SCRIPT_PATH/nginx/sites-available/gradido.conf
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $SCRIPT_PATH/nginx/sites-available/update-page.conf.template > $SCRIPT_PATH/nginx/sites-available/update-page.conf envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $SCRIPT_PATH/nginx/sites-available/update-page.conf.template > $SCRIPT_PATH/nginx/sites-available/update-page.conf
mkdir $SCRIPT_PATH/nginx/sites-enabled mkdir -p $SCRIPT_PATH/nginx/sites-enabled
ln -s $SCRIPT_PATH/nginx/sites-available/update-page.conf $SCRIPT_PATH/nginx/sites-enabled/default ln -sf $SCRIPT_PATH/nginx/sites-available/update-page.conf $SCRIPT_PATH/nginx/sites-enabled/default
ln -s $SCRIPT_PATH/nginx/sites-enabled/default /etc/nginx/sites-enabled ln -sf $SCRIPT_PATH/nginx/sites-enabled/default /etc/nginx/sites-enabled
ln -s $SCRIPT_PATH/nginx/common /etc/nginx/ ln -sf $SCRIPT_PATH/nginx/common /etc/nginx/
rmdir /etc/nginx/conf.d if [ -e /etc/nginx/conf.d ] && [ ! -L /etc/nginx/conf.d ]; then
ln -s $SCRIPT_PATH/nginx/conf.d /etc/nginx/ rm -rf /etc/nginx/conf.d
ln -s $SCRIPT_PATH/nginx/conf.d /etc/nginx/
fi
# Make nginx restart automatic # Make nginx restart automatic
mkdir /etc/systemd/system/nginx.service.d mkdir -p /etc/systemd/system/nginx.service.d
# Define the content to be put into the override.conf file # Define the content to be put into the override.conf file
CONFIG_CONTENT="[Unit] CONFIG_CONTENT="[Unit]
StartLimitIntervalSec=500 StartLimitIntervalSec=500
@ -124,11 +210,17 @@ sudo systemctl daemon-reload
# setup https with certbot # setup https with certbot
certbot certonly --nginx --non-interactive --agree-tos --domains $COMMUNITY_HOST --email $COMMUNITY_SUPPORT_MAIL certbot certonly --nginx --non-interactive --agree-tos --domains $COMMUNITY_HOST --email $COMMUNITY_SUPPORT_MAIL
export NVM_DIR="/home/gradido/.nvm"
BUN_VERSION_FILE="$PROJECT_ROOT/.bun-version"
if [ ! -f "$BUN_VERSION_FILE" ]; then
echo ".bun-version file not found at: $BUN_VERSION_FILE"
exit 1
fi
export BUN_VERSION="$(cat "$BUN_VERSION_FILE" | tr -d '[:space:]')"
export BUN_INSTALL="/home/gradido/.bun"
# run as gradido user (until EOF) # run as gradido user (until EOF)
sudo -u gradido bash <<'EOF' sudo -u gradido bash <<EOF
export NVM_DIR="/home/gradido/.nvm"
NODE_VERSION="v18.20.7"
export NVM_DIR
# Install nvm if it doesn't exist # Install nvm if it doesn't exist
if [ ! -d "$NVM_DIR" ]; then if [ ! -d "$NVM_DIR" ]; then
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
@ -137,15 +229,26 @@ sudo -u gradido bash <<'EOF'
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
# Install Node if not already installed # Install Node if not already installed
if ! nvm ls $NODE_VERSION >/dev/null 2>&1; then if ! nvm ls >/dev/null 2>&1; then
nvm install $NODE_VERSION nvm install
fi fi
# Install yarn and pm2 # Install pm2 and turbo
npm i -g yarn pm2 npm i -g pm2 turbo
# start pm2
pm2 startup echo "'bun' v$BUN_VERSION will be installed now!"
curl -fsSL https://bun.com/install | bash -s "bun-v${BUN_VERSION}"
EOF EOF
# Load bun
export BUN_INSTALL="/home/gradido/.bun"
export PATH="$BUN_INSTALL/bin:$PATH"
# Load nvm
export NVM_DIR="/home/gradido/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
# start pm2
pm2 startup
# Install logrotate # Install logrotate
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $SCRIPT_PATH/logrotate/gradido.conf.template > $SCRIPT_PATH/logrotate/gradido.conf envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $SCRIPT_PATH/logrotate/gradido.conf.template > $SCRIPT_PATH/logrotate/gradido.conf
cp $SCRIPT_PATH/logrotate/gradido.conf /etc/logrotate.d/gradido.conf cp $SCRIPT_PATH/logrotate/gradido.conf /etc/logrotate.d/gradido.conf
@ -153,15 +256,15 @@ cp $SCRIPT_PATH/logrotate/gradido.conf /etc/logrotate.d/gradido.conf
# create db user # create db user
export DB_USER=gradido export DB_USER=gradido
# create a new password only if it not already exist # create a new password only if it not already exist
if [ -z "${DB_PASSWORD}" ]; then : "${DB_PASSWORD:=$(tr -dc '_A-Za-z0-9' < /dev/urandom | head -c 32)}"
export DB_PASSWORD=$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c 32; echo);
fi
# Check if DB_PASSWORD is still empty, then exit with an error # Check if DB_PASSWORD is still empty, then exit with an error
if [ -z "${DB_PASSWORD}" ]; then if [ -z "${DB_PASSWORD}" ]; then
echo "Error: Failed to generate DB_PASSWORD." echo "Error: Failed to generate DB_PASSWORD."
exit 1 exit 1
fi fi
export DB_PASSWORD
mysql <<EOFMYSQL mysql <<EOFMYSQL
CREATE USER IF NOT EXISTS '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASSWORD'; CREATE USER IF NOT EXISTS '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASSWORD';
GRANT ALL PRIVILEGES ON *.* TO '$DB_USER'@'localhost'; GRANT ALL PRIVILEGES ON *.* TO '$DB_USER'@'localhost';
@ -195,5 +298,4 @@ chown -R gradido:gradido $PROJECT_ROOT
sudo -u gradido crontab < $LOCAL_SCRIPT_DIR/crontabs.txt sudo -u gradido crontab < $LOCAL_SCRIPT_DIR/crontabs.txt
# Start gradido # Start gradido
# Note: on first startup some errors will occur - nothing serious
sudo -u gradido $SCRIPT_PATH/start.sh $1 sudo -u gradido $SCRIPT_PATH/start.sh $1

View File

@ -1,6 +1,3 @@
# must match the CONFIG_VERSION.EXPECTED definition in scr/config/index.ts
CONFIG_VERSION=$FEDERATION_DHT_CONFIG_VERSION
# Database # Database
DB_HOST=127.0.0.1 DB_HOST=127.0.0.1
DB_PORT=3306 DB_PORT=3306
@ -14,7 +11,6 @@ COMMUNITY_NAME=$COMMUNITY_NAME
COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION
# Federation # Federation
FEDERATION_DHT_CONFIG_VERSION=$FEDERATION_DHT_CONFIG_VERSION
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen # if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
# on an hash created from this topic # on an hash created from this topic
FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC

View File

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

View File

@ -37,7 +37,7 @@ export const configSchema = v.object({
GRADIDO_BLOCKCHAIN_SERVER_CRYPTO_KEY: hex16Schema, GRADIDO_BLOCKCHAIN_SERVER_CRYPTO_KEY: hex16Schema,
HOME_COMMUNITY_SEED: v.pipe( HOME_COMMUNITY_SEED: v.pipe(
hexSchema, hexSchema,
v.length(64, 'expect seed length minimum 64 characters (32 Bytes)'), v.length(64, 'expect seed length 64 characters (32 Bytes)'),
v.transform<string, MemoryBlock>((input: string) => MemoryBlock.fromHex(input)), v.transform<string, MemoryBlock>((input: string) => MemoryBlock.fromHex(input)),
), ),
HIERO_HEDERA_NETWORK: v.optional( HIERO_HEDERA_NETWORK: v.optional(

View File

@ -0,0 +1,273 @@
<mxfile host="Electron" modified="2025-12-18T01:25:36.977Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/22.0.3 Chrome/114.0.5735.289 Electron/25.8.4 Safari/537.36" etag="ulUa-DwD6iMx1WLL9hxg" version="22.0.3" type="device">
<diagram name="Seite-1" id="pXcQQGq2mbEeDNBOBd4l">
<mxGraphModel dx="1206" dy="702" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="EPUhzRXaTLY6ULSqPNZg-4" value="recepient: backend:&lt;br&gt;TransactionLinkResolver" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="80" y="40" width="140" height="920" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-5" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-4">
<mxGeometry x="65" y="70" width="10" height="80" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-6" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;queryTransactionLink&lt;/div&gt;" style="html=1;verticalAlign=bottom;startArrow=oval;endArrow=block;startSize=8;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" target="EPUhzRXaTLY6ULSqPNZg-5" parent="EPUhzRXaTLY6ULSqPNZg-4">
<mxGeometry x="-0.2381" y="5" relative="1" as="geometry">
<mxPoint x="-40" y="75" as="sourcePoint" />
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-7" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-4">
<mxGeometry x="65" y="160" width="10" height="40" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-8" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;queryRedeemJwtLink&lt;/div&gt;" style="html=1;align=left;spacingLeft=2;endArrow=block;rounded=0;edgeStyle=orthogonalEdgeStyle;curved=0;rounded=0;" edge="1" target="EPUhzRXaTLY6ULSqPNZg-7" parent="EPUhzRXaTLY6ULSqPNZg-4">
<mxGeometry relative="1" as="geometry">
<mxPoint x="70" y="140" as="sourcePoint" />
<Array as="points">
<mxPoint x="100" y="170" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-11" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-4">
<mxGeometry x="65" y="230" width="10" height="80" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-12" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;redeemTransactionLink&lt;/div&gt;" style="html=1;verticalAlign=bottom;startArrow=oval;endArrow=block;startSize=8;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" target="EPUhzRXaTLY6ULSqPNZg-11" parent="EPUhzRXaTLY6ULSqPNZg-4">
<mxGeometry x="-0.4286" y="5" relative="1" as="geometry">
<mxPoint x="-40" y="235" as="sourcePoint" />
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-13" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-4">
<mxGeometry x="65" y="393" width="10" height="367" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-14" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;&lt;div style=&quot;line-height: 19px;&quot;&gt;disburseTransactionLink&lt;/div&gt;&lt;/div&gt;" style="html=1;verticalAlign=bottom;startArrow=oval;endArrow=block;startSize=8;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" parent="EPUhzRXaTLY6ULSqPNZg-4" target="EPUhzRXaTLY6ULSqPNZg-13">
<mxGeometry x="-0.4286" y="5" relative="1" as="geometry">
<mxPoint x="-40" y="398" as="sourcePoint" />
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-9" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;TransactionLink&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="140" y="170" as="sourcePoint" />
<mxPoint x="40" y="170" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-10" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;RedeemJwtLink&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="140" y="230" as="sourcePoint" />
<mxPoint x="40" y="230" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-15" value="backend:&lt;br&gt;:TransactionReslover" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="240" y="40" width="120" height="920" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-16" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-15">
<mxGeometry x="55" y="290" width="10" height="50" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-17" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;executeTransaction&lt;/div&gt;" style="html=1;verticalAlign=bottom;startArrow=oval;endArrow=block;startSize=8;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" target="EPUhzRXaTLY6ULSqPNZg-16" parent="1" source="EPUhzRXaTLY6ULSqPNZg-11">
<mxGeometry relative="1" as="geometry">
<mxPoint x="265" y="335" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-18" value="sender: federation:&lt;br&gt;:DisbursementResolver" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="380" y="40" width="130" height="920" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-19" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-18">
<mxGeometry x="60" y="400" width="10" height="320" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-20" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;processDisburseJwtOnSenderCommunity&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" target="EPUhzRXaTLY6ULSqPNZg-19" parent="1" source="EPUhzRXaTLY6ULSqPNZg-13">
<mxGeometry relative="1" as="geometry">
<mxPoint x="430" y="445" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-22" value="sender: core:&lt;br&gt;processXComSendCoins" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="570" y="40" width="140" height="1080" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-23" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-22">
<mxGeometry x="65" y="410" width="10" height="640" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-26" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-22">
<mxGeometry x="68" y="435" width="10" height="225" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-27" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;processXComPendingSendCoins&lt;/div&gt;" style="html=1;align=left;spacingLeft=2;endArrow=block;rounded=0;edgeStyle=orthogonalEdgeStyle;curved=0;rounded=0;" edge="1" target="EPUhzRXaTLY6ULSqPNZg-26" parent="EPUhzRXaTLY6ULSqPNZg-22">
<mxGeometry relative="1" as="geometry">
<mxPoint x="70" y="420" as="sourcePoint" />
<Array as="points">
<mxPoint x="100" y="420" />
<mxPoint x="100" y="450" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-43" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-22">
<mxGeometry x="68" y="710" width="10" height="270" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-44" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;processXComCommittingSendCoins&lt;/div&gt;" style="html=1;align=left;spacingLeft=2;endArrow=block;rounded=0;edgeStyle=orthogonalEdgeStyle;curved=0;rounded=0;" edge="1" target="EPUhzRXaTLY6ULSqPNZg-43" parent="EPUhzRXaTLY6ULSqPNZg-22">
<mxGeometry relative="1" as="geometry">
<mxPoint x="73" y="690" as="sourcePoint" />
<Array as="points">
<mxPoint x="103" y="720" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-24" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;processXComCompleteTransaction&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" target="EPUhzRXaTLY6ULSqPNZg-23" parent="1" source="EPUhzRXaTLY6ULSqPNZg-19">
<mxGeometry x="-0.027" y="5" relative="1" as="geometry">
<mxPoint x="595" y="455" as="sourcePoint" />
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-28" value="recepient: federation:&lt;br&gt;SendCoinsResolver" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="840" y="40" width="120" height="1080" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-29" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-28">
<mxGeometry x="55" y="488" width="10" height="50" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-30" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;voteForSendCoins&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" target="EPUhzRXaTLY6ULSqPNZg-29" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="648" y="533" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-32" value="recepient: database:&lt;br&gt;PendingTransaction" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="960" y="480" width="120" height="120" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-33" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-32">
<mxGeometry x="55" y="58" width="10" height="30" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-35" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=-5;" edge="1" source="EPUhzRXaTLY6ULSqPNZg-33" parent="1" target="EPUhzRXaTLY6ULSqPNZg-29">
<mxGeometry relative="1" as="geometry">
<mxPoint x="945" y="613" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-34" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;insert&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" target="EPUhzRXaTLY6ULSqPNZg-33" parent="1" source="EPUhzRXaTLY6ULSqPNZg-29">
<mxGeometry relative="1" as="geometry">
<mxPoint x="945" y="543" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-31" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=-5;" edge="1" source="EPUhzRXaTLY6ULSqPNZg-29" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="648" y="573" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-36" value="sender: database:&lt;br&gt;PendingTransaction" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="715" y="588" width="120" height="100" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-37" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-36">
<mxGeometry x="55" y="60" width="10" height="30" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-38" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=-5;" edge="1" parent="1" source="EPUhzRXaTLY6ULSqPNZg-37">
<mxGeometry relative="1" as="geometry">
<mxPoint x="648" y="673" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-39" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;insert&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" parent="1" target="EPUhzRXaTLY6ULSqPNZg-37">
<mxGeometry relative="1" as="geometry">
<mxPoint x="648" y="653" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-21" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=-5;" edge="1" source="EPUhzRXaTLY6ULSqPNZg-19" parent="1" target="EPUhzRXaTLY6ULSqPNZg-13">
<mxGeometry relative="1" as="geometry">
<mxPoint x="370" y="515" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-41" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;" edge="1" parent="1" target="EPUhzRXaTLY6ULSqPNZg-23">
<mxGeometry relative="1" as="geometry">
<mxPoint x="648" y="690" as="sourcePoint" />
<mxPoint x="740" y="690" as="targetPoint" />
<Array as="points">
<mxPoint x="670" y="690" />
<mxPoint x="670" y="720" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-46" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="895" y="788" width="10" height="132" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-47" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;&lt;div style=&quot;line-height: 19px;&quot;&gt;settleSendCoins&lt;/div&gt;&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" parent="1" target="EPUhzRXaTLY6ULSqPNZg-46">
<mxGeometry relative="1" as="geometry">
<mxPoint x="648" y="793" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-48" value="recepient: database:&lt;br&gt;Transaction" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="1210" y="740" width="120" height="100" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-49" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-48">
<mxGeometry x="55" y="60" width="10" height="30" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-50" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=-5;" edge="1" parent="1" source="EPUhzRXaTLY6ULSqPNZg-49" target="EPUhzRXaTLY6ULSqPNZg-58">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1153" y="823" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-51" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;insert&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;exitX=1;exitY=0;exitDx=0;exitDy=5;exitPerimeter=0;" edge="1" parent="1" source="EPUhzRXaTLY6ULSqPNZg-59" target="EPUhzRXaTLY6ULSqPNZg-49">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1153" y="803" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-52" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=-5;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="648" y="910" as="targetPoint" />
<mxPoint x="895" y="910" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-53" value="sender: database:&lt;br&gt;PendingTransaction" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="715" y="920" width="120" height="100" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-54" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-53">
<mxGeometry x="55" y="60" width="10" height="30" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-55" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=-5;" edge="1" parent="1" source="EPUhzRXaTLY6ULSqPNZg-54" target="EPUhzRXaTLY6ULSqPNZg-43">
<mxGeometry relative="1" as="geometry">
<mxPoint x="648" y="995" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-56" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;insert&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" parent="1" target="EPUhzRXaTLY6ULSqPNZg-54" source="EPUhzRXaTLY6ULSqPNZg-43">
<mxGeometry relative="1" as="geometry">
<mxPoint x="648" y="975" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-57" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;" edge="1" parent="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="648" y="1012" as="sourcePoint" />
<mxPoint x="645" y="1042" as="targetPoint" />
<Array as="points">
<mxPoint x="670" y="1012" />
<mxPoint x="670" y="1042" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-58" value="recepient: federation:&lt;br&gt;settlePendingReceiveTransaction" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="990" y="740" width="200" height="220" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-59" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-58">
<mxGeometry x="95" y="60" width="10" height="110" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-60" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=-5;" edge="1" parent="1" source="EPUhzRXaTLY6ULSqPNZg-59" target="EPUhzRXaTLY6ULSqPNZg-28">
<mxGeometry relative="1" as="geometry">
<mxPoint x="905" y="823" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-61" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;&lt;div style=&quot;line-height: 19px;&quot;&gt;settlePendingReceiveTransaction&lt;/div&gt;&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;" edge="1" parent="1" target="EPUhzRXaTLY6ULSqPNZg-59">
<mxGeometry relative="1" as="geometry">
<mxPoint x="905" y="803" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-62" value="recepient: database:&lt;br&gt;PendingTransaction" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="1">
<mxGeometry x="1300" y="810" width="120" height="100" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-63" value="" style="html=1;points=[[0,0,0,0,5],[0,1,0,0,-5],[1,0,0,0,5],[1,1,0,0,-5]];perimeter=orthogonalPerimeter;outlineConnect=0;targetShapes=umlLifeline;portConstraint=eastwest;newEdgeStyle={&quot;curved&quot;:0,&quot;rounded&quot;:0};" vertex="1" parent="EPUhzRXaTLY6ULSqPNZg-62">
<mxGeometry x="55" y="60" width="10" height="30" as="geometry" />
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-64" value="return" style="html=1;verticalAlign=bottom;endArrow=open;dashed=1;endSize=8;curved=0;rounded=0;exitX=0;exitY=1;exitDx=0;exitDy=-5;exitPerimeter=0;" edge="1" parent="1" source="EPUhzRXaTLY6ULSqPNZg-63" target="EPUhzRXaTLY6ULSqPNZg-59">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1160" y="907.5" as="targetPoint" />
<mxPoint x="1335" y="907.5" as="sourcePoint" />
</mxGeometry>
</mxCell>
<mxCell id="EPUhzRXaTLY6ULSqPNZg-65" value="&lt;div style=&quot;color: rgb(59, 59, 59); font-family: Consolas, &amp;quot;Courier New&amp;quot;, monospace; line-height: 19px; font-size: 10px;&quot;&gt;settled&lt;/div&gt;" style="html=1;verticalAlign=bottom;endArrow=block;curved=0;rounded=0;entryX=0;entryY=0;entryDx=0;entryDy=5;entryPerimeter=0;" edge="1" parent="1" source="EPUhzRXaTLY6ULSqPNZg-59" target="EPUhzRXaTLY6ULSqPNZg-63">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1165" y="887.5" as="sourcePoint" />
<mxPoint x="1335" y="887.5" as="targetPoint" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@ -1,6 +1,3 @@
# must match the CONFIG_VERSION.EXPECTED definition in scr/config/index.ts
CONFIG_VERSION=$FEDERATION_CONFIG_VERSION
LOG_LEVEL=$LOG_LEVEL LOG_LEVEL=$LOG_LEVEL
# this is set fix to false, because it is important for 'production' environments. only set to true if a graphql-playground should be in use # this is set fix to false, because it is important for 'production' environments. only set to true if a graphql-playground should be in use
GRAPHIQL=false GRAPHIQL=false

View File

@ -1,6 +1,6 @@
{ {
"name": "federation", "name": "federation",
"version": "2.7.3", "version": "2.7.4",
"description": "Gradido federation module providing Gradido-Hub-Federation and versioned API for inter community communication", "description": "Gradido federation module providing Gradido-Hub-Federation and versioned API for inter community communication",
"main": "src/index.ts", "main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/federation", "repository": "https://github.com/gradido/gradido/federation",

View File

@ -0,0 +1,17 @@
import { CommandExecutor, CommandResult, EncryptedTransferArgs } from 'core'
import { Arg, Ctx, Mutation, Resolver } from 'type-graphql'
@Resolver()
export class CommandResolver {
private commandExecutor = new CommandExecutor()
@Mutation(() => CommandResult)
async sendCommand(
@Arg('encryptedArgs', () => EncryptedTransferArgs) encryptedArgs: any,
@Ctx() context: any,
): Promise<CommandResult> {
// Convert to EncryptedTransferArgs if needed
const result = await this.commandExecutor.executeEncryptedCommand(encryptedArgs)
return result as unknown as CommandResult
}
}

View File

@ -1,5 +1,6 @@
import { NonEmptyArray } from 'type-graphql' import { NonEmptyArray } from 'type-graphql'
import { AuthenticationResolver } from './resolver/AuthenticationResolver' import { AuthenticationResolver } from './resolver/AuthenticationResolver'
import { CommandResolver } from './resolver/CommandResolver'
import { DisbursementResolver } from './resolver/DisbursementResolver' import { DisbursementResolver } from './resolver/DisbursementResolver'
import { PublicCommunityInfoResolver } from './resolver/PublicCommunityInfoResolver' import { PublicCommunityInfoResolver } from './resolver/PublicCommunityInfoResolver'
import { PublicKeyResolver } from './resolver/PublicKeyResolver' import { PublicKeyResolver } from './resolver/PublicKeyResolver'
@ -8,6 +9,7 @@ import { SendCoinsResolver } from './resolver/SendCoinsResolver'
export const getApiResolvers = (): NonEmptyArray<Function> => { export const getApiResolvers = (): NonEmptyArray<Function> => {
return [ return [
AuthenticationResolver, AuthenticationResolver,
CommandResolver,
DisbursementResolver, DisbursementResolver,
PublicCommunityInfoResolver, PublicCommunityInfoResolver,
PublicKeyResolver, PublicKeyResolver,

View File

@ -1,9 +1,16 @@
import { NonEmptyArray } from 'type-graphql' import { NonEmptyArray } from 'type-graphql'
import { AuthenticationResolver } from '../1_0/resolver/AuthenticationResolver' import { AuthenticationResolver } from '../1_0/resolver/AuthenticationResolver'
import { CommandResolver } from '../1_0/resolver/CommandResolver'
import { PublicCommunityInfoResolver } from '../1_0/resolver/PublicCommunityInfoResolver' import { PublicCommunityInfoResolver } from '../1_0/resolver/PublicCommunityInfoResolver'
import { SendCoinsResolver } from '../1_0/resolver/SendCoinsResolver' import { SendCoinsResolver } from '../1_0/resolver/SendCoinsResolver'
import { PublicKeyResolver } from './resolver/PublicKeyResolver' import { PublicKeyResolver } from './resolver/PublicKeyResolver'
export const getApiResolvers = (): NonEmptyArray<Function> => { export const getApiResolvers = (): NonEmptyArray<Function> => {
return [AuthenticationResolver, PublicCommunityInfoResolver, PublicKeyResolver, SendCoinsResolver] return [
AuthenticationResolver,
CommandResolver,
PublicCommunityInfoResolver,
PublicKeyResolver,
SendCoinsResolver,
]
} }

View File

@ -1,6 +1,7 @@
import 'source-map-support/register' import 'source-map-support/register'
import { defaultCategory, initLogger } from 'config-schema' import { defaultCategory, initLogger } from 'config-schema'
import { initializeCommands } from 'core'
import { getLogger } from 'log4js' import { getLogger } from 'log4js'
import { onShutdown, printServerCrashAsciiArt, ShutdownReason } from 'shared' import { onShutdown, printServerCrashAsciiArt, ShutdownReason } from 'shared'
// config // config
@ -44,6 +45,8 @@ async function main() {
} }
}) })
}) })
initializeCommands()
} }
main().catch((e) => { main().catch((e) => {

View File

@ -1,5 +1,3 @@
CONFIG_VERSION=$FRONTEND_CONFIG_VERSION
# Endpoints # Endpoints
GRAPHQL_PATH=$GRAPHQL_PATH GRAPHQL_PATH=$GRAPHQL_PATH
ADMIN_AUTH_PATH=$ADMIN_AUTH_PATH ADMIN_AUTH_PATH=$ADMIN_AUTH_PATH

View File

@ -1,6 +1,6 @@
{ {
"name": "frontend", "name": "frontend",
"version": "2.7.3", "version": "2.7.4",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "concurrently \"yarn watch-scss\" \"vite\"", "dev": "concurrently \"yarn watch-scss\" \"vite\"",
@ -16,8 +16,8 @@
"test:coverage": "cross-env TZ=UTC vitest run --coverage", "test:coverage": "cross-env TZ=UTC vitest run --coverage",
"test:debug": "cross-env TZ=UTC node --inspect-brk ./node_modules/vitest/vitest.mjs", "test:debug": "cross-env TZ=UTC node --inspect-brk ./node_modules/vitest/vitest.mjs",
"test:watch": "cross-env TZ=UTC vitest", "test:watch": "cross-env TZ=UTC vitest",
"locales": "scripts/sort.sh", "locales": "bun scripts/sortLocales.ts",
"locales:fix": "scripts/sort.sh --fix", "locales:fix": "bun scripts/sortLocales.ts --fix",
"compile-scss": "node ./scripts/scss.mjs compile", "compile-scss": "node ./scripts/scss.mjs compile",
"watch-scss": "node ./scripts/scss.mjs watch", "watch-scss": "node ./scripts/scss.mjs watch",
"compile-scss-sass": "node ./scripts/scss.mjs compile sass", "compile-scss-sass": "node ./scripts/scss.mjs compile sass",

View File

@ -0,0 +1,51 @@
#!/usr/bin/env bun
import { readdir, readFile, writeFile } from 'fs/promises'
import { join } from 'path'
const ROOT_DIR = join(import.meta.dir, '..')
const LOCALES_DIR = join(ROOT_DIR, 'src', 'locales')
const FIX = process.argv.includes('--fix')
function sortObject(value: any): any {
if (Array.isArray(value)) {
return value.map(sortObject)
}
if (value && typeof value === 'object') {
return Object.keys(value)
.sort()
.reduce<Record<string, any>>((acc, key) => {
acc[key] = sortObject(value[key])
return acc
}, {})
}
return value
}
let exitCode = 0
const files = (await readdir(LOCALES_DIR))
.filter(f => f.endsWith('.json'))
for (const file of files) {
const path = join(LOCALES_DIR, file)
const originalText = await readFile(path, 'utf8')
const originalJson = JSON.parse(originalText)
const sortedJson = sortObject(originalJson)
const sortedText = JSON.stringify(sortedJson, null, 2) + '\n'
if (originalText !== sortedText) {
if (FIX) {
await writeFile(path, sortedText)
} else {
console.error(`${file} is not sorted by keys`)
exitCode = 1
}
}
}
process.exit(exitCode)

View File

@ -126,6 +126,8 @@ const form = reactive({ ...entityDataToForm.value })
const now = ref(new Date()) // checked every minute, updated if day, month or year changed const now = ref(new Date()) // checked every minute, updated if day, month or year changed
const disableSmartValidState = ref(false) const disableSmartValidState = ref(false)
// set to true after submit, to disable submit button
const submitted = ref(false)
const minimalDate = computed(() => useMinimalContributionDate(now.value)) const minimalDate = computed(() => useMinimalContributionDate(now.value))
const isThisMonth = computed(() => { const isThisMonth = computed(() => {
@ -195,7 +197,7 @@ const validationSchema = computed(() => {
}) })
}) })
const disabled = computed(() => !validationSchema.value.isValidSync(form)) const disabled = computed(() => !validationSchema.value.isValidSync(form) || submitted.value)
// decide message if no open creation exists // decide message if no open creation exists
const noOpenCreation = computed(() => { const noOpenCreation = computed(() => {
@ -243,6 +245,7 @@ const updateField = (newValue, name) => {
} }
function submit() { function submit() {
submitted.value = true
emit('upsert-contribution', toRaw(form)) emit('upsert-contribution', toRaw(form))
} }
</script> </script>

View File

@ -1,21 +1,21 @@
{ {
"85": "85%",
"100": "100%",
"125": "125%",
"(": "(", "(": "(",
")": ")", ")": ")",
"100": "100%",
"1000thanks": "1000 Dank, weil du bei uns bist!", "1000thanks": "1000 Dank, weil du bei uns bist!",
"125": "125%",
"85": "85%",
"Chat": "Chat", "Chat": "Chat",
"GDD": "GDD", "GDD": "GDD",
"GDD-long": "Gradido", "GDD-long": "Gradido",
"GDT": "GDT", "GDT": "GDT",
"GMS": { "GMS": {
"title": "Geo Matching System GMS (in Entwicklung)", "desc": "Finde Mitglieder aller Communities auf einer Landkarte.",
"desc": "Finde Mitglieder aller Communities auf einer Landkarte." "title": "Geo Matching System GMS (in Entwicklung)"
}, },
"Humhub": { "Humhub": {
"title": "Gradido-Kreise", "desc": "Gemeinsam unterstützen wir einander achtsam in Kreiskultur.",
"desc": "Gemeinsam unterstützen wir einander achtsam in Kreiskultur." "title": "Gradido-Kreise"
}, },
"PersonalDetails": "Persönliche Angaben", "PersonalDetails": "Persönliche Angaben",
"advanced-calculation": "Vorausberechnung", "advanced-calculation": "Vorausberechnung",
@ -34,27 +34,27 @@
}, },
"back": "Zurück", "back": "Zurück",
"card-circles": { "card-circles": {
"headline": "Kooperationsplattform »Gradido-Kreise«",
"text": "Lokale Kreise, Studienkreise, Projekte, Events und Kongresse",
"allowed": { "allowed": {
"button": "Kreise öffnen..." "button": "Kreise öffnen..."
}, },
"headline": "Kooperationsplattform »Gradido-Kreise«",
"not-allowed": { "not-allowed": {
"button": "Konfigurieren..." "button": "Konfigurieren..."
} },
"text": "Lokale Kreise, Studienkreise, Projekte, Events und Kongresse"
}, },
"card-user-search": { "card-user-search": {
"headline": "Geografische Mitgliedssuche (beta)",
"allowed": { "allowed": {
"button": "Mitgliedssuche öffnen...", "button": "Mitgliedssuche öffnen...",
"disabled-button": "GMS offline...", "disabled-button": "GMS offline...",
"text": "Finde Mitglieder aller Communities auf einer Landkarte." "text": "Finde Mitglieder aller Communities auf einer Landkarte."
}, },
"headline": "Geografische Mitgliedssuche (beta)",
"info": "So gehts",
"not-allowed": { "not-allowed": {
"button": "Standort eintragen...", "button": "Standort eintragen...",
"text": "Um andere Mitglieder in deinem Umkreis zu finden, trage jetzt deinen Standort auf der Karte ein!" "text": "Um andere Mitglieder in deinem Umkreis zu finden, trage jetzt deinen Standort auf der Karte ein!"
}, }
"info": "So gehts"
}, },
"community": { "community": {
"admins": "Administratoren", "admins": "Administratoren",
@ -95,8 +95,8 @@
"lastContribution": "Letzte Beiträge", "lastContribution": "Letzte Beiträge",
"noContributions": { "noContributions": {
"allContributions": "Es wurden noch keine Beiträge eingereicht.", "allContributions": "Es wurden noch keine Beiträge eingereicht.",
"myContributions": "Du hast noch keine Beiträge eingereicht.", "emptyPage": "Diese Seite ist leer.",
"emptyPage": "Diese Seite ist leer." "myContributions": "Du hast noch keine Beiträge eingereicht."
}, },
"noOpenCreation": { "noOpenCreation": {
"allMonth": "Für alle beiden Monate ist dein Schöpfungslimit erreicht. Den Nächsten Monat kannst du wieder 1000 GDD Schöpfen.", "allMonth": "Für alle beiden Monate ist dein Schöpfungslimit erreicht. Den Nächsten Monat kannst du wieder 1000 GDD Schöpfen.",
@ -117,6 +117,7 @@
"copy-to-clipboard": "In Zwischenablage kopieren", "copy-to-clipboard": "In Zwischenablage kopieren",
"creation": "Schöpfen", "creation": "Schöpfen",
"decay": { "decay": {
"Starting_block_decay": "Startblock Vergänglichkeit",
"before_startblock_transaction": "Diese Transaktion beinhaltet keine Vergänglichkeit.", "before_startblock_transaction": "Diese Transaktion beinhaltet keine Vergänglichkeit.",
"calculation_decay": "Berechnung der Vergänglichkeit", "calculation_decay": "Berechnung der Vergänglichkeit",
"calculation_total": "Berechnung der Gesamtsumme", "calculation_total": "Berechnung der Gesamtsumme",
@ -127,7 +128,6 @@
"new_balance": "Neuer Kontostand", "new_balance": "Neuer Kontostand",
"old_balance": "Vorheriger Kontostand", "old_balance": "Vorheriger Kontostand",
"past_time": "Vergangene Zeit", "past_time": "Vergangene Zeit",
"Starting_block_decay": "Startblock Vergänglichkeit",
"total": "Gesamt", "total": "Gesamt",
"types": { "types": {
"creation": "Geschöpft", "creation": "Geschöpft",
@ -186,8 +186,8 @@
"link_decay_description": "Der Link-Betrag wird zusammen mit der maximalen Vergänglichkeit blockiert. Nach Einlösen, Verfallen oder Löschen des Links wird der Rest wieder freigegeben.", "link_decay_description": "Der Link-Betrag wird zusammen mit der maximalen Vergänglichkeit blockiert. Nach Einlösen, Verfallen oder Löschen des Links wird der Rest wieder freigegeben.",
"memo": "Nachricht", "memo": "Nachricht",
"message": "Nachricht", "message": "Nachricht",
"new_balance": "Neuer Kontostand nach Bestätigung",
"newPasswordRepeat": "Neues Passwort wiederholen", "newPasswordRepeat": "Neues Passwort wiederholen",
"new_balance": "Neuer Kontostand nach Bestätigung",
"no_gdd_available": "Du hast keine GDD zum versenden.", "no_gdd_available": "Du hast keine GDD zum versenden.",
"ok": "Ok", "ok": "Ok",
"password": "Passwort", "password": "Passwort",
@ -201,11 +201,11 @@
"reset": "Zurücksetzen", "reset": "Zurücksetzen",
"save": "Speichern", "save": "Speichern",
"scann_code": "<strong>QR Code Scanner</strong> - Scanne den QR Code deines Partners", "scann_code": "<strong>QR Code Scanner</strong> - Scanne den QR Code deines Partners",
"sender": "Absender",
"send_check": "Bestätige deine Transaktion. Prüfe bitte nochmal alle Angaben!", "send_check": "Bestätige deine Transaktion. Prüfe bitte nochmal alle Angaben!",
"send_now": "Jetzt senden", "send_now": "Jetzt senden",
"send_transaction_error": "Leider konnte die Transaktion nicht ausgeführt werden!", "send_transaction_error": "Leider konnte die Transaktion nicht ausgeführt werden!",
"send_transaction_success": "Deine Transaktion wurde erfolgreich ausgeführt", "send_transaction_success": "Deine Transaktion wurde erfolgreich ausgeführt",
"sender": "Absender",
"sorry": "Entschuldigung", "sorry": "Entschuldigung",
"thx": "Danke", "thx": "Danke",
"to": "bis", "to": "bis",
@ -214,28 +214,28 @@
"username-placeholder": "Wähle deinen Benutzernamen", "username-placeholder": "Wähle deinen Benutzernamen",
"validation": { "validation": {
"amount": { "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.", "decimal-places": "Der Betrag sollte maximal zwei Nachkommastellen enthalten.",
"max": "Der Betrag sollte höchstens {max} groß sein.",
"min": "Der Betrag sollte mindestens {min} groß sein.",
"typeError": "Der Betrag sollte eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein." "typeError": "Der Betrag sollte eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein."
}, },
"contributionDate": { "contributionDate": {
"required": "Das Beitragsdatum ist ein Pflichtfeld.", "max": "Das Späteste Beitragsdatum ist heute, der {max}.",
"min": "Das Frühste Beitragsdatum ist {min}.", "min": "Das Frühste Beitragsdatum ist {min}.",
"max": "Das Späteste Beitragsdatum ist heute, der {max}." "required": "Das Beitragsdatum ist ein Pflichtfeld."
}, },
"contributionMemo": { "contributionMemo": {
"min": "Die Tätigkeitsbeschreibung sollte mindestens {min} Zeichen lang sein.",
"max": "Die Tätigkeitsbeschreibung sollte höchstens {max} Zeichen lang sein.", "max": "Die Tätigkeitsbeschreibung sollte höchstens {max} Zeichen lang sein.",
"min": "Die Tätigkeitsbeschreibung sollte mindestens {min} Zeichen lang sein.",
"required": "Die Tätigkeitsbeschreibung ist ein Pflichtfeld." "required": "Die Tätigkeitsbeschreibung ist ein Pflichtfeld."
}, },
"gddSendAmount": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein.",
"hours": { "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.", "decimal-places": "Die Stunden sollten maximal zwei Nachkommastellen enthalten.",
"max": "Die Stunden sollten höchstens {max} groß sein.",
"min": "Die Stunden sollten mindestens {min} groß sein.",
"typeError": "Die Stunden sollten eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein." "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": { "identifier": {
"communityIsReachable": "Community nicht gefunden oder nicht erreichbar!", "communityIsReachable": "Community nicht gefunden oder nicht erreichbar!",
"required": "Der Empfänger ist ein Pflichtfeld.", "required": "Der Empfänger ist ein Pflichtfeld.",
@ -243,8 +243,8 @@
}, },
"is-not": "Du kannst dir selbst keine Gradidos überweisen!", "is-not": "Du kannst dir selbst keine Gradidos überweisen!",
"memo": { "memo": {
"min": "Die Nachricht sollte mindestens {min} Zeichen lang sein.",
"max": "Die Nachricht sollte höchstens {max} Zeichen lang sein.", "max": "Die Nachricht sollte höchstens {max} Zeichen lang sein.",
"min": "Die Nachricht sollte mindestens {min} Zeichen lang sein.",
"required": "Die Nachricht ist ein Pflichtfeld." "required": "Die Nachricht ist ein Pflichtfeld."
}, },
"requiredField": "{fieldName} ist ein Pflichtfeld", "requiredField": "{fieldName} ist ein Pflichtfeld",
@ -347,13 +347,13 @@
}, },
"openHours": "Offene Stunden", "openHours": "Offene Stunden",
"pageTitle": { "pageTitle": {
"circles": "Gradido Kreise",
"contributions": "Gradido schöpfen", "contributions": "Gradido schöpfen",
"gdt": "Deine GDT Transaktionen", "gdt": "Deine GDT Transaktionen",
"information": "{community}", "information": "{community}",
"overview": "Willkommen {name}", "overview": "Willkommen {name}",
"send": "Sende Gradidos", "send": "Sende Gradidos",
"settings": "Einstellungen", "settings": "Einstellungen",
"circles": "Gradido Kreise",
"transactions": "Deine Transaktionen", "transactions": "Deine Transaktionen",
"usersearch": "Geografische Mitgliedssuche (beta)" "usersearch": "Geografische Mitgliedssuche (beta)"
}, },
@ -367,8 +367,6 @@
"warningText": "Bist du noch da?" "warningText": "Bist du noch da?"
}, },
"settings": { "settings": {
"community": "Kreise & Mitgliedssuche",
"emailInfo": "Kann aktuell noch nicht geändert werden.",
"GMS": { "GMS": {
"disabled": "Daten werden nicht nach GMS exportiert", "disabled": "Daten werden nicht nach GMS exportiert",
"enabled": "Daten werden nach GMS exportiert", "enabled": "Daten werden nach GMS exportiert",
@ -383,20 +381,22 @@
"communityCoords": "Ihr Gemeinschafts-Standort: Breitengrad {lat}, Längengrad {lng}", "communityCoords": "Ihr Gemeinschafts-Standort: Breitengrad {lat}, Längengrad {lng}",
"communityLocationLabel": "Ihr Gemeinschafts-Standort", "communityLocationLabel": "Ihr Gemeinschafts-Standort",
"headline": "Geografische Standorterfassung des Benutzers", "headline": "Geografische Standorterfassung des Benutzers",
"search": "Nach einem Standort suchen",
"userCoords": "Ihr Standort: Breitengrad {lat}, Längengrad {lng}", "userCoords": "Ihr Standort: Breitengrad {lat}, Längengrad {lng}",
"userLocationLabel": "Ihr Standort", "userLocationLabel": "Ihr Standort"
"search": "Nach einem Standort suchen"
}, },
"naming-format": "Namen anzeigen:", "naming-format": "Namen anzeigen:",
"publish-location": { "publish-location": {
"exact": "Genaue Position",
"approximate": "Ungefähre Position", "approximate": "Ungefähre Position",
"exact": "Genaue Position",
"updated": "Positionstyp für GMS aktualisiert" "updated": "Positionstyp für GMS aktualisiert"
}, },
"publish-name": { "publish-name": {
"updated": "Namensformat für GMS aktualisiert" "updated": "Namensformat für GMS aktualisiert"
} }
}, },
"community": "Kreise & Mitgliedssuche",
"emailInfo": "Kann aktuell noch nicht geändert werden.",
"hideAmountGDD": "Dein GDD Betrag ist versteckt.", "hideAmountGDD": "Dein GDD Betrag ist versteckt.",
"hideAmountGDT": "Dein GDT Betrag ist versteckt.", "hideAmountGDT": "Dein GDT Betrag ist versteckt.",
"humhub": { "humhub": {
@ -448,9 +448,9 @@
"alias-or-initials": "Benutzername oder Initialen", "alias-or-initials": "Benutzername oder Initialen",
"alias-or-initials-tooltip": "Benutzername, falls vorhanden, oder die Initialen von Vorname und Nachname jeweils die ersten zwei Buchstaben", "alias-or-initials-tooltip": "Benutzername, falls vorhanden, oder die Initialen von Vorname und Nachname jeweils die ersten zwei Buchstaben",
"first": "Vorname", "first": "Vorname",
"first-tooltip": "Nur der Vornamen",
"first-initial": "Vorname und Initial", "first-initial": "Vorname und Initial",
"first-initial-tooltip": "Vornamen plus den ersten Anfangsbuchstaben des Nachnamens", "first-initial-tooltip": "Vornamen plus den ersten Anfangsbuchstaben des Nachnamens",
"first-tooltip": "Nur der Vornamen",
"initials": "Initialen", "initials": "Initialen",
"initials-tooltip": "Initialen von Vor- und Nachname also jeweils die ersten zwei Buchstaben unabhängig von der Existenz des Benutzernamens", "initials-tooltip": "Initialen von Vor- und Nachname also jeweils die ersten zwei Buchstaben unabhängig von der Existenz des Benutzernamens",
"name-full": "Vorname und Nachname", "name-full": "Vorname und Nachname",
@ -507,9 +507,9 @@
"send_you": "sendet dir" "send_you": "sendet dir"
}, },
"usersearch": { "usersearch": {
"button": "Starte die Nutzersuche...",
"headline": "Geografische Nutzersuche", "headline": "Geografische Nutzersuche",
"text": "Ganz gleich zu welcher Community du gehörst, mit dem Geo Matching System findest du Mitglieder aller Communities auf einer Landkarte. Du kannst nach Angeboten und Bedürfnissen filtern und dir die Nutzer anzeigen lassen, die zu Dir passen.\n\nMit dem Button wird ein neues Browser-Fenster geöffnet, in dem dir die Nutzer in deinem Umfeld auf einer Karte angezeigt werden.", "text": "Ganz gleich zu welcher Community du gehörst, mit dem Geo Matching System findest du Mitglieder aller Communities auf einer Landkarte. Du kannst nach Angeboten und Bedürfnissen filtern und dir die Nutzer anzeigen lassen, die zu Dir passen.\n\nMit dem Button wird ein neues Browser-Fenster geöffnet, in dem dir die Nutzer in deinem Umfeld auf einer Karte angezeigt werden."
"button": "Starte die Nutzersuche..."
}, },
"via_link": "über einen Link", "via_link": "über einen Link",
"welcome": "Willkommen in der Gemeinschaft" "welcome": "Willkommen in der Gemeinschaft"

View File

@ -1,21 +1,21 @@
{ {
"85": "85%",
"100": "100%",
"125": "125%",
"(": "(", "(": "(",
")": ")", ")": ")",
"100": "100%",
"1000thanks": "1000 thanks for being with us!", "1000thanks": "1000 thanks for being with us!",
"125": "125%",
"85": "85%",
"Chat": "Chat", "Chat": "Chat",
"GDD": "GDD", "GDD": "GDD",
"GDD-long": "Gradido", "GDD-long": "Gradido",
"GDT": "GDT", "GDT": "GDT",
"GMS": { "GMS": {
"title": "Geo Matching System GMS (in develop)", "desc": "Find members of all communities on a map.",
"desc": "Find members of all communities on a map." "title": "Geo Matching System GMS (in develop)"
}, },
"Humhub": { "Humhub": {
"title": "Gradido-circles", "desc": "Together we support each other - mindful in circle culture.",
"desc": "Together we support each other - mindful in circle culture." "title": "Gradido-circles"
}, },
"PersonalDetails": "Personal details", "PersonalDetails": "Personal details",
"advanced-calculation": "Advanced calculation", "advanced-calculation": "Advanced calculation",
@ -34,27 +34,27 @@
}, },
"back": "Back", "back": "Back",
"card-circles": { "card-circles": {
"headline": "Cooperation platform “Gradido Circles”",
"text": "Local circles, study circles, projects, events and congresses",
"allowed": { "allowed": {
"button": "Open Circles..." "button": "Open Circles..."
}, },
"headline": "Cooperation platform “Gradido Circles”",
"not-allowed": { "not-allowed": {
"button": "Configurate..." "button": "Configurate..."
} },
"text": "Local circles, study circles, projects, events and congresses"
}, },
"card-user-search": { "card-user-search": {
"headline": "Geographic member search (beta)",
"allowed": { "allowed": {
"button": "Open member search...", "button": "Open member search...",
"disabled-button": "GMS offline...", "disabled-button": "GMS offline...",
"text": "Find Members of all Communities on a Map." "text": "Find Members of all Communities on a Map."
}, },
"headline": "Geographic member search (beta)",
"info": "How it works",
"not-allowed": { "not-allowed": {
"button": "Enter location...", "button": "Enter location...",
"text": "To find other members in your area, enter your location on the map now!" "text": "To find other members in your area, enter your location on the map now!"
}, }
"info": "How it works"
}, },
"community": { "community": {
"admins": "Administrators", "admins": "Administrators",
@ -95,8 +95,8 @@
"lastContribution": "Last Contributions", "lastContribution": "Last Contributions",
"noContributions": { "noContributions": {
"allContributions": "No contributions have been submitted yet.", "allContributions": "No contributions have been submitted yet.",
"myContributions": "You have not submitted any entries yet.", "emptyPage": "This page is empty.",
"emptyPage": "This page is empty." "myContributions": "You have not submitted any entries yet."
}, },
"noOpenCreation": { "noOpenCreation": {
"allMonth": "For all two months your creation limit is reached. The next month you can create 1000 GDD again.", "allMonth": "For all two months your creation limit is reached. The next month you can create 1000 GDD again.",
@ -117,6 +117,7 @@
"copy-to-clipboard": "Copy to clipboard", "copy-to-clipboard": "Copy to clipboard",
"creation": "Creation", "creation": "Creation",
"decay": { "decay": {
"Starting_block_decay": "Starting Block Decay",
"before_startblock_transaction": "This transaction does not include decay.", "before_startblock_transaction": "This transaction does not include decay.",
"calculation_decay": "Calculation of Decay", "calculation_decay": "Calculation of Decay",
"calculation_total": "Calculation of the total Amount", "calculation_total": "Calculation of the total Amount",
@ -127,7 +128,6 @@
"new_balance": "New balance", "new_balance": "New balance",
"old_balance": "Previous balance", "old_balance": "Previous balance",
"past_time": "Time passed", "past_time": "Time passed",
"Starting_block_decay": "Starting Block Decay",
"total": "Total", "total": "Total",
"types": { "types": {
"creation": "Created", "creation": "Created",
@ -186,8 +186,8 @@
"link_decay_description": "The link amount is blocked along with the maximum decay. After the link has been redeemed, expired, or deleted, the rest is released again.", "link_decay_description": "The link amount is blocked along with the maximum decay. After the link has been redeemed, expired, or deleted, the rest is released again.",
"memo": "Message", "memo": "Message",
"message": "Message", "message": "Message",
"new_balance": "Account balance after confirmation",
"newPasswordRepeat": "Repeat new password", "newPasswordRepeat": "Repeat new password",
"new_balance": "Account balance after confirmation",
"no_gdd_available": "You do not have GDD to send.", "no_gdd_available": "You do not have GDD to send.",
"ok": "Ok", "ok": "Ok",
"password": "Password", "password": "Password",
@ -201,11 +201,11 @@
"reset": "Reset", "reset": "Reset",
"save": "Save", "save": "Save",
"scann_code": "<strong>QR Code Scanner</strong> - Scan the QR Code of your partner", "scann_code": "<strong>QR Code Scanner</strong> - Scan the QR Code of your partner",
"sender": "Sender",
"send_check": "Confirm your transaction. Please check all data again!", "send_check": "Confirm your transaction. Please check all data again!",
"send_now": "Send now", "send_now": "Send now",
"send_transaction_error": "Unfortunately, the transaction could not be executed!", "send_transaction_error": "Unfortunately, the transaction could not be executed!",
"send_transaction_success": "Your transaction was successfully completed", "send_transaction_success": "Your transaction was successfully completed",
"sender": "Sender",
"sorry": "Sorry", "sorry": "Sorry",
"thx": "Thank you", "thx": "Thank you",
"to": "to", "to": "to",
@ -214,28 +214,28 @@
"username-placeholder": "Choose your username", "username-placeholder": "Choose your username",
"validation": { "validation": {
"amount": { "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.", "decimal-places": "The amount should contain a maximum of two decimal places.",
"max": "The amount should not be larger than {max}.",
"min": "The amount should be at least {min} in size.",
"typeError": "The amount should be a number between {min} and {max} with at most two digits after the decimal point." "typeError": "The amount should be a number between {min} and {max} with at most two digits after the decimal point."
}, },
"contributionDate": { "contributionDate": {
"required": "The contribution date is a required field.", "max": "The latest contribution date is today, {max}.",
"min": "The earliest contribution date is {min}.", "min": "The earliest contribution date is {min}.",
"max": "The latest contribution date is today, {max}." "required": "The contribution date is a required field."
}, },
"contributionMemo": { "contributionMemo": {
"min": "The job description should be at least {min} characters long.",
"max": "The job description should not be longer than {max} characters.", "max": "The job description should not be longer than {max} characters.",
"min": "The job description should be at least {min} characters long.",
"required": "The job description is required." "required": "The job description is required."
}, },
"gddSendAmount": "The {_field_} field must be a number between {min} and {max} with at most two digits after the decimal point.",
"hours": { "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.", "decimal-places": "The hours should contain a maximum of two decimal places.",
"max": "The hours should not be larger than {max}.",
"min": "The hours should be at least {min} in size.",
"typeError": "The hours should be a number between {min} and {max} with at most two digits after the decimal point." "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": { "identifier": {
"communityIsReachable": "Community not found our not reachable!", "communityIsReachable": "Community not found our not reachable!",
"required": "The recipient is a required field.", "required": "The recipient is a required field.",
@ -243,8 +243,8 @@
}, },
"is-not": "You cannot send Gradidos to yourself!", "is-not": "You cannot send Gradidos to yourself!",
"memo": { "memo": {
"min": "The message should be at least {min} characters long.",
"max": "The message should not be longer than {max} characters.", "max": "The message should not be longer than {max} characters.",
"min": "The message should be at least {min} characters long.",
"required": "The message is required." "required": "The message is required."
}, },
"requiredField": "The {fieldName} field is required", "requiredField": "The {fieldName} field is required",
@ -347,6 +347,7 @@
}, },
"openHours": "Open Hours", "openHours": "Open Hours",
"pageTitle": { "pageTitle": {
"circles": "Gradido Circles",
"contributions": "Create Gradido", "contributions": "Create Gradido",
"gdt": "Your GDT transactions", "gdt": "Your GDT transactions",
"information": "{community}", "information": "{community}",
@ -354,7 +355,6 @@
"send": "Send Gradidos", "send": "Send Gradidos",
"settings": "Settings", "settings": "Settings",
"transactions": "Your transactions", "transactions": "Your transactions",
"circles": "Gradido Circles",
"usersearch": "Geographic member search (beta)" "usersearch": "Geographic member search (beta)"
}, },
"qrCode": "QR Code", "qrCode": "QR Code",
@ -367,8 +367,6 @@
"warningText": "Are you still there?" "warningText": "Are you still there?"
}, },
"settings": { "settings": {
"community": "Community",
"emailInfo": "Cannot be changed at this time.",
"GMS": { "GMS": {
"disabled": "Data not exported to GMS", "disabled": "Data not exported to GMS",
"enabled": "Data exported to GMS", "enabled": "Data exported to GMS",
@ -383,20 +381,22 @@
"communityCoords": "Your Community Location: Lat {lat}, Lng {lng}", "communityCoords": "Your Community Location: Lat {lat}, Lng {lng}",
"communityLocationLabel": "Your Community-Location", "communityLocationLabel": "Your Community-Location",
"headline": "Geographic Location-Capturing of the User", "headline": "Geographic Location-Capturing of the User",
"search": "Search for a location",
"userCoords": "Your Location: Lat {lat}, Lng {lng}", "userCoords": "Your Location: Lat {lat}, Lng {lng}",
"userLocationLabel": "Your Location", "userLocationLabel": "Your Location"
"search": "Search for a location"
}, },
"naming-format": "Show Name:", "naming-format": "Show Name:",
"publish-location": { "publish-location": {
"exact": "exact position",
"approximate": "approximate position", "approximate": "approximate position",
"exact": "exact position",
"updated": "format of location for GMS updated" "updated": "format of location for GMS updated"
}, },
"publish-name": { "publish-name": {
"updated": "format of name for GMS updated" "updated": "format of name for GMS updated"
} }
}, },
"community": "Community",
"emailInfo": "Cannot be changed at this time.",
"hideAmountGDD": "Your GDD amount is hidden.", "hideAmountGDD": "Your GDD amount is hidden.",
"hideAmountGDT": "Your GDT amount is hidden.", "hideAmountGDT": "Your GDT amount is hidden.",
"humhub": { "humhub": {
@ -448,9 +448,9 @@
"alias-or-initials": "Username or initials (Default)", "alias-or-initials": "Username or initials (Default)",
"alias-or-initials-tooltip": "username, if available, or the initials of the first name and last name, the first two letters in each case", "alias-or-initials-tooltip": "username, if available, or the initials of the first name and last name, the first two letters in each case",
"first": "Firstname", "first": "Firstname",
"first-tooltip": "the first name only",
"first-initial": "First name and initial", "first-initial": "First name and initial",
"first-initial-tooltip": "first name plus the first letter of the last name", "first-initial-tooltip": "first name plus the first letter of the last name",
"first-tooltip": "the first name only",
"initials": "Initials", "initials": "Initials",
"initials-tooltip": "Initials of first name and last name, i.e. the first two letters of each regardless of the existence of the user name", "initials-tooltip": "Initials of first name and last name, i.e. the first two letters of each regardless of the existence of the user name",
"name-full": "first name and last name", "name-full": "first name and last name",
@ -507,9 +507,9 @@
"send_you": "wants to send you" "send_you": "wants to send you"
}, },
"usersearch": { "usersearch": {
"button": "Start user search... ",
"headline": "Geographical User Search", "headline": "Geographical User Search",
"text": "No matter which community you belong to, with the Geo Matching System you can find members of all communities on a map. You can filter according to offers and needs and display the users that match you.\n\nThe button opens a new browser window in which the users in your area are displayed on a map.", "text": "No matter which community you belong to, with the Geo Matching System you can find members of all communities on a map. You can filter according to offers and needs and display the users that match you.\n\nThe button opens a new browser window in which the users in your area are displayed on a map."
"button": "Start user search... "
}, },
"via_link": "via Link", "via_link": "via Link",
"welcome": "Welcome to the community" "welcome": "Welcome to the community"

View File

@ -1,8 +1,8 @@
{ {
"100": "100%",
"1000thanks": "1000 Gracias, por estar con nosotros!",
"125": "125%",
"85": "85%", "85": "85%",
"100": "100%",
"125": "125%",
"1000thanks": "1000 Gracias, por estar con nosotros!",
"GDD": "GDD", "GDD": "GDD",
"GDT": "GDT", "GDT": "GDT",
"PersonalDetails": "Datos personales", "PersonalDetails": "Datos personales",
@ -22,32 +22,32 @@
}, },
"back": "Volver", "back": "Volver",
"card-circles": { "card-circles": {
"headline": "Plataforma de cooperación «Círculos Gradido»",
"text": "Círculos locales, círculos de estudio, proyectos, ev entos y congresos",
"allowed": { "allowed": {
"button": "Abrir círculos..." "button": "Abrir círculos..."
}, },
"headline": "Plataforma de cooperación «Círculos Gradido»",
"not-allowed": { "not-allowed": {
"button": "Configurar..." "button": "Configurar..."
} },
"text": "Círculos locales, círculos de estudio, proyectos, ev entos y congresos"
}, },
"card-user-search": { "card-user-search": {
"headline": "Búsqueda geográfica de miembros (beta)",
"allowed": { "allowed": {
"button": "Abrir búsqueda de miembros...", "button": "Abrir búsqueda de miembros...",
"disabled-button": "GMS offline...", "disabled-button": "GMS offline...",
"text": "Encuentra Miembros de todas las Comunidades en un Mapa." "text": "Encuentra Miembros de todas las Comunidades en un Mapa."
}, },
"headline": "Búsqueda geográfica de miembros (beta)",
"info": "Así se hace",
"not-allowed": { "not-allowed": {
"button": "Introducir ubicación...", "button": "Introducir ubicación...",
"text": "Para encontrar otros miembros cerca de ti, ¡introduce ahora tu ubicación en el mapa!" "text": "Para encontrar otros miembros cerca de ti, ¡introduce ahora tu ubicación en el mapa!"
}, }
"info": "Así se hace"
}, },
"circles": { "circles": {
"button": "Abrir Círculos...",
"headline": "Juntos nos apoyamos - atentos a la cultura de los círculos.", "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.", "text": "Haga clic en el botón para abrir la plataforma de cooperación en una nueva ventana del navegador."
"button": "Abrir Círculos..."
}, },
"community": { "community": {
"admins": "Administradores", "admins": "Administradores",
@ -60,8 +60,8 @@
"moderators": "Moderadores", "moderators": "Moderadores",
"myContributions": "Mis contribuciones al bien común", "myContributions": "Mis contribuciones al bien común",
"noOpenContributionLinkText": "Actualmente no hay creaciones generadas por enlaces.", "noOpenContributionLinkText": "Actualmente no hay creaciones generadas por enlaces.",
"openContributionLinks": "Creaciones generadas por enlace",
"openContributionLinkText": "Para créditos iniciales o fines similares, la comunidad puede crear los llamados enlaces de creación. Éstos activan creaciones automáticas que se acreditan al usuario.\nLa comunidad \"{name}\" admite actualmente {count} creaciones generadas por enlaces:", "openContributionLinkText": "Para créditos iniciales o fines similares, la comunidad puede crear los llamados enlaces de creación. Éstos activan creaciones automáticas que se acreditan al usuario.\nLa comunidad \"{name}\" admite actualmente {count} creaciones generadas por enlaces:",
"openContributionLinks": "Creaciones generadas por enlace",
"other-communities": "Otras comunidades", "other-communities": "Otras comunidades",
"startNewsButton": "Enviar Gradidos", "startNewsButton": "Enviar Gradidos",
"statistic": "Estadísticas", "statistic": "Estadísticas",
@ -103,6 +103,7 @@
}, },
"copy-to-clipboard": "Copiar al portapapeles", "copy-to-clipboard": "Copiar al portapapeles",
"decay": { "decay": {
"Starting_block_decay": "Startblock disminución gradual",
"before_startblock_transaction": "Esta transacción no implica disminución en su valor.", "before_startblock_transaction": "Esta transacción no implica disminución en su valor.",
"calculation_decay": "Cálculo de la disminución gradual del valor", "calculation_decay": "Cálculo de la disminución gradual del valor",
"calculation_total": "Cálculo de la suma total", "calculation_total": "Cálculo de la suma total",
@ -111,7 +112,6 @@
"decay_since_last_transaction": "Disminución gradual", "decay_since_last_transaction": "Disminución gradual",
"last_transaction": "Transacción anterior", "last_transaction": "Transacción anterior",
"past_time": "Tiempo transcurrido", "past_time": "Tiempo transcurrido",
"Starting_block_decay": "Startblock disminución gradual",
"total": "Total", "total": "Total",
"types": { "types": {
"creation": "Creado", "creation": "Creado",
@ -166,8 +166,8 @@
"link_decay_description": "El importe del enlace se bloquea junto con la ransience máxima. Una vez que el enlace se ha canjeado, caducado o eliminado, el resto se libera de nuevo.", "link_decay_description": "El importe del enlace se bloquea junto con la ransience máxima. Una vez que el enlace se ha canjeado, caducado o eliminado, el resto se libera de nuevo.",
"memo": "Mensaje", "memo": "Mensaje",
"message": "Noticia", "message": "Noticia",
"new_balance": "Saldo de cuenta nuevo depués de confirmación",
"newPasswordRepeat": "Repetir contraseña nueva", "newPasswordRepeat": "Repetir contraseña nueva",
"new_balance": "Saldo de cuenta nuevo depués de confirmación",
"no_gdd_available": "No dispones de GDD para enviar.", "no_gdd_available": "No dispones de GDD para enviar.",
"password": "Contraseña", "password": "Contraseña",
"passwordRepeat": "Repetir contraseña", "passwordRepeat": "Repetir contraseña",
@ -179,11 +179,11 @@
"reset": "Restablecer", "reset": "Restablecer",
"save": "Guardar", "save": "Guardar",
"scann_code": "<strong>QR Code Scanner</strong> - Escanea el código QR de tu pareja", "scann_code": "<strong>QR Code Scanner</strong> - Escanea el código QR de tu pareja",
"sender": "Remitente",
"send_check": "Confirma tu transacción. Por favor revisa toda la información nuevamente!", "send_check": "Confirma tu transacción. Por favor revisa toda la información nuevamente!",
"send_now": "Enviar ahora", "send_now": "Enviar ahora",
"send_transaction_error": "Desafortunadamente, la transacción no se pudo ejecutar!", "send_transaction_error": "Desafortunadamente, la transacción no se pudo ejecutar!",
"send_transaction_success": "Su transacción fue ejecutada con éxito", "send_transaction_success": "Su transacción fue ejecutada con éxito",
"sender": "Remitente",
"sorry": "Disculpa", "sorry": "Disculpa",
"thx": "Gracias", "thx": "Gracias",
"time": "Tiempo", "time": "Tiempo",
@ -295,6 +295,7 @@
}, },
"openHours": "Open Hours", "openHours": "Open Hours",
"pageTitle": { "pageTitle": {
"circles": "Círculos Gradido",
"contributions": "Cuchara Gradido", "contributions": "Cuchara Gradido",
"gdt": "Tu GDT Transacciones", "gdt": "Tu GDT Transacciones",
"information": "{community}", "information": "{community}",
@ -302,7 +303,6 @@
"send": "Enviar Gradidos", "send": "Enviar Gradidos",
"settings": "Soporte", "settings": "Soporte",
"transactions": "Tu Transacciones", "transactions": "Tu Transacciones",
"circles": "Círculos Gradido",
"usersearch": "Búsqueda geográfica de miembros (beta)" "usersearch": "Búsqueda geográfica de miembros (beta)"
}, },
"qrCode": "Código QR", "qrCode": "Código QR",
@ -326,10 +326,10 @@
"communityCoords": "Ubicación de tu comunidad: Lat {lat}, Lng {lng}", "communityCoords": "Ubicación de tu comunidad: Lat {lat}, Lng {lng}",
"communityLocationLabel": "Ubicación de tu comunidad", "communityLocationLabel": "Ubicación de tu comunidad",
"headline": "Captura de ubicación geográfica del usuario", "headline": "Captura de ubicación geográfica del usuario",
"userCoords": "Tu ubicación: Lat {lat}, Lng {lng}",
"userLocationLabel": "Tu ubicación",
"search": "Buscar una ubicación", "search": "Buscar una ubicación",
"success": "Ubicación guardada exitosamente" "success": "Ubicación guardada exitosamente",
"userCoords": "Tu ubicación: Lat {lat}, Lng {lng}",
"userLocationLabel": "Tu ubicación"
} }
}, },
"language": { "language": {
@ -414,9 +414,9 @@
"send_you": "te envía" "send_you": "te envía"
}, },
"usersearch": { "usersearch": {
"button": "Iniciar la búsqueda de usuarios...",
"headline": "Búsqueda geográfica de usuarios", "headline": "Búsqueda geográfica de usuarios",
"text": "No importa a qué comunidad pertenezcas, con el Geo Matching System puedes encontrar miembros de todas las comunidades en un mapa. Puedes filtrar según ofertas y requisitos y visualizar los usuarios que coinciden con tu perfil.\n\nEl botón abre una nueva ventana del navegador en la que se muestran en un mapa los usuarios de tu zona.", "text": "No importa a qué comunidad pertenezcas, con el Geo Matching System puedes encontrar miembros de todas las comunidades en un mapa. Puedes filtrar según ofertas y requisitos y visualizar los usuarios que coinciden con tu perfil.\n\nEl botón abre una nueva ventana del navegador en la que se muestran en un mapa los usuarios de tu zona."
"button": "Iniciar la búsqueda de usuarios..."
}, },
"via_link": "atraves de un enlace", "via_link": "atraves de un enlace",
"welcome": "Bienvenido a la comunidad." "welcome": "Bienvenido a la comunidad."

View File

@ -1,10 +1,10 @@
{ {
"85": "85%",
"100": "100%",
"125": "125%",
"(": "(", "(": "(",
")": ")", ")": ")",
"100": "100%",
"1000thanks": "1000 mercis d'être avec nous!", "1000thanks": "1000 mercis d'être avec nous!",
"125": "125%",
"85": "85%",
"GDD": "GDD", "GDD": "GDD",
"GDT": "GDT", "GDT": "GDT",
"PersonalDetails": "Informations personnelles", "PersonalDetails": "Informations personnelles",
@ -24,32 +24,32 @@
}, },
"back": "Retour", "back": "Retour",
"card-circles": { "card-circles": {
"headline": "Plate-forme de coopération «Cercles Gradido»",
"text": "Cercles locaux, cercles d'études, projets, événements et congrès",
"allowed": { "allowed": {
"button": "Ouvrir les cercles..." "button": "Ouvrir les cercles..."
}, },
"headline": "Plate-forme de coopération «Cercles Gradido»",
"not-allowed": { "not-allowed": {
"button": "Configurer..." "button": "Configurer..."
} },
"text": "Cercles locaux, cercles d'études, projets, événements et congrès"
}, },
"card-user-search": { "card-user-search": {
"headline": "Recherche géographique de membres (bêta)",
"allowed": { "allowed": {
"button": "Ouvrir la recherche de membres...", "button": "Ouvrir la recherche de membres...",
"disabled-button": "GMS offline...", "disabled-button": "GMS offline...",
"text": "Trouve des membres de toutes les communautés sur une carte." "text": "Trouve des membres de toutes les communautés sur une carte."
}, },
"headline": "Recherche géographique de membres (bêta)",
"info": "Comment ça marche",
"not-allowed": { "not-allowed": {
"button": "Indiquer ta position...", "button": "Indiquer ta position...",
"text": "Pour trouver d'autres membres près de chez toi, indique dès maintenant ta position sur la carte!" "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": { "circles": {
"button": "Ouvrir les Cercles...",
"headline": "Ensemble, nous nous soutenons mutuellement - attentifs à la culture du cercle.", "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.", "text": "En cliquant sur le bouton, tu ouvres la plateforme de coopération dans une nouvelle fenêtre de navigation."
"button": "Ouvrir les Cercles..."
}, },
"community": { "community": {
"admins": "Administrateurs", "admins": "Administrateurs",
@ -61,8 +61,8 @@
"moderators": "Modérateurs", "moderators": "Modérateurs",
"myContributions": "Mes contributions", "myContributions": "Mes contributions",
"noOpenContributionLinkText": "Actuellement, il n'y a pas de créations générées par lien.", "noOpenContributionLinkText": "Actuellement, il n'y a pas de créations générées par lien.",
"openContributionLinks": "Créations générées par lien",
"openContributionLinkText": "Pour les crédits de départ ou à des fins similaires, la communauté peut créer des \"liens de création\". Ils déclenchent des créations automatiques qui sont créditées à l'utilisateur.\nLa communauté \"{name}\" soutient actuellement {count} créations générées par lien:", "openContributionLinkText": "Pour les crédits de départ ou à des fins similaires, la communauté peut créer des \"liens de création\". Ils déclenchent des créations automatiques qui sont créditées à l'utilisateur.\nLa communauté \"{name}\" soutient actuellement {count} créations générées par lien:",
"openContributionLinks": "Créations générées par lien",
"startNewsButton": "Envoyer des gradidos", "startNewsButton": "Envoyer des gradidos",
"submitContribution": "Contribuer" "submitContribution": "Contribuer"
}, },
@ -106,6 +106,7 @@
"copy-to-clipboard": "Copier dans le presse-papier", "copy-to-clipboard": "Copier dans le presse-papier",
"creation": "Création", "creation": "Création",
"decay": { "decay": {
"Starting_block_decay": "Début de la décroissance",
"before_startblock_transaction": "Cette transaction n'est pas péremptoire.", "before_startblock_transaction": "Cette transaction n'est pas péremptoire.",
"calculation_decay": "Calcul de la décroissance", "calculation_decay": "Calcul de la décroissance",
"calculation_total": "Calcul du montant total", "calculation_total": "Calcul du montant total",
@ -114,7 +115,6 @@
"decay_since_last_transaction": "Décroissance depuis la dernière transaction", "decay_since_last_transaction": "Décroissance depuis la dernière transaction",
"last_transaction": "Dernière transaction:", "last_transaction": "Dernière transaction:",
"past_time": "Le temps a expiré", "past_time": "Le temps a expiré",
"Starting_block_decay": "Début de la décroissance",
"total": "Total", "total": "Total",
"types": { "types": {
"creation": "Créé", "creation": "Créé",
@ -172,8 +172,8 @@
"link_decay_description": "Le montant du lien est bloqué avec le dépérissement maximale. Une fois le lien utilisé, expiré ou supprimé, le reste est à nouveau débloqué.", "link_decay_description": "Le montant du lien est bloqué avec le dépérissement maximale. Une fois le lien utilisé, expiré ou supprimé, le reste est à nouveau débloqué.",
"memo": "Note", "memo": "Note",
"message": "Message", "message": "Message",
"new_balance": "Montant du solde après confirmation",
"newPasswordRepeat": "Répétez le nouveau mot de passe", "newPasswordRepeat": "Répétez le nouveau mot de passe",
"new_balance": "Montant du solde après confirmation",
"no_gdd_available": "Vous n'avez pas de GDD à envoyer.", "no_gdd_available": "Vous n'avez pas de GDD à envoyer.",
"password": "Mot de passe", "password": "Mot de passe",
"passwordRepeat": "Répétez le mot de passe", "passwordRepeat": "Répétez le mot de passe",
@ -185,11 +185,11 @@
"reset": "Réinitialiser", "reset": "Réinitialiser",
"save": "Sauvegarder", "save": "Sauvegarder",
"scann_code": "<strong>QR Code Scanner</strong> - Scannez le QR code de votre partenaire", "scann_code": "<strong>QR Code Scanner</strong> - Scannez le QR code de votre partenaire",
"sender": "Expéditeur",
"send_check": "Confirmez la transaction. Veuillez revérifier toutes les données svp!", "send_check": "Confirmez la transaction. Veuillez revérifier toutes les données svp!",
"send_now": "Envoyez maintenant", "send_now": "Envoyez maintenant",
"send_transaction_error": "Malheureusement, la transaction n'a pas pu être effectuée!", "send_transaction_error": "Malheureusement, la transaction n'a pas pu être effectuée!",
"send_transaction_success": "Votre transaction a été effectuée avec succès", "send_transaction_success": "Votre transaction a été effectuée avec succès",
"sender": "Expéditeur",
"sorry": "Désolé", "sorry": "Désolé",
"thx": "Merci", "thx": "Merci",
"to": "à", "to": "à",
@ -303,6 +303,7 @@
}, },
"openHours": "Heures ouverte", "openHours": "Heures ouverte",
"pageTitle": { "pageTitle": {
"circles": "Cercles Gradido",
"contributions": "Ma communauté", "contributions": "Ma communauté",
"gdt": "Vos transactions GDT", "gdt": "Vos transactions GDT",
"information": "{community}", "information": "{community}",
@ -310,7 +311,6 @@
"send": "Envoyé Gradidos", "send": "Envoyé Gradidos",
"settings": "Configuration", "settings": "Configuration",
"transactions": "Vos transactions", "transactions": "Vos transactions",
"circles": "Cercles Gradido",
"usersearch": "Recherche géographique de membres (bèta)" "usersearch": "Recherche géographique de membres (bèta)"
}, },
"qrCode": "QR Code", "qrCode": "QR Code",
@ -334,10 +334,10 @@
"communityCoords": "Emplacement de votre communauté : Lat {lat}, Long {lng}", "communityCoords": "Emplacement de votre communauté : Lat {lat}, Long {lng}",
"communityLocationLabel": "Emplacement de votre communauté", "communityLocationLabel": "Emplacement de votre communauté",
"headline": "Capture de la localisation géographique de l'utilisateur", "headline": "Capture de la localisation géographique de l'utilisateur",
"userCoords": "Votre emplacement : Lat {lat}, Long {lng}",
"userLocationLabel": "Votre emplacement",
"search": "Rechercher un emplacement", "search": "Rechercher un emplacement",
"success": "Emplacement enregistré avec succès" "success": "Emplacement enregistré avec succès",
"userCoords": "Votre emplacement : Lat {lat}, Long {lng}",
"userLocationLabel": "Votre emplacement"
} }
}, },
"hideAmountGDD": "Votre montant GDD est caché.", "hideAmountGDD": "Votre montant GDD est caché.",
@ -422,9 +422,9 @@
"send_you": "veut vous envoyer" "send_you": "veut vous envoyer"
}, },
"usersearch": { "usersearch": {
"button": "Commence la recherche d'utilisateurs...",
"headline": "Recherche géographique d'utilisateurs", "headline": "Recherche géographique d'utilisateurs",
"text": "Quelle que soit la communauté à laquelle tu appartiens, le système de géo-matching te permet de trouver des membres de toutes les communautés sur une carte géographique. Tu peux filtrer selon les offres et les besoins et afficher les utilisateurs qui te correspondent.\n\nEn cliquant sur le bouton, une nouvelle fenêtre de navigateur s'ouvre et t'affiche les utilisateurs de ton entourage sur une carte.", "text": "Quelle que soit la communauté à laquelle tu appartiens, le système de géo-matching te permet de trouver des membres de toutes les communautés sur une carte géographique. Tu peux filtrer selon les offres et les besoins et afficher les utilisateurs qui te correspondent.\n\nEn cliquant sur le bouton, une nouvelle fenêtre de navigateur s'ouvre et t'affiche les utilisateurs de ton entourage sur une carte."
"button": "Commence la recherche d'utilisateurs..."
}, },
"via_link": "par lien", "via_link": "par lien",
"welcome": "Bienvenu dans la communauté" "welcome": "Bienvenu dans la communauté"

View File

@ -1,8 +1,8 @@
{ {
"100": "100%",
"1000thanks": "1000 dank, omdat je bij ons bent!",
"125": "125%",
"85": "85%", "85": "85%",
"100": "100%",
"125": "125%",
"1000thanks": "1000 dank, omdat je bij ons bent!",
"GDD": "GDD", "GDD": "GDD",
"GDT": "GDT", "GDT": "GDT",
"PersonalDetails": "Persoonlijke gegevens", "PersonalDetails": "Persoonlijke gegevens",
@ -22,32 +22,32 @@
}, },
"back": "Terug", "back": "Terug",
"card-circles": { "card-circles": {
"headline": "Samenwerkingsplatform “Gradido Kringen”",
"text": "Lokale kringen, studiekringen, projecten, evenementen en congressen",
"allowed": { "allowed": {
"button": "Kringen openen..." "button": "Kringen openen..."
}, },
"headline": "Samenwerkingsplatform “Gradido Kringen”",
"not-allowed": { "not-allowed": {
"button": "Configureren..." "button": "Configureren..."
} },
"text": "Lokale kringen, studiekringen, projecten, evenementen en congressen"
}, },
"card-user-search": { "card-user-search": {
"headline": "Geografisch leden zoeken (bèta)",
"allowed": { "allowed": {
"button": "Leden zoeken openen...", "button": "Leden zoeken openen...",
"disabled-button": "GMS offline...", "disabled-button": "GMS offline...",
"text": "Vind leden van alle gemeenschappen op een kaart." "text": "Vind leden van alle gemeenschappen op een kaart."
}, },
"headline": "Geografisch leden zoeken (bèta)",
"info": "Zo gaat dat",
"not-allowed": { "not-allowed": {
"button": "Locatie invoeren", "button": "Locatie invoeren",
"text": "Om andere leden in jouw omgeving te vinden, voer nu je locatie in op de kaart!" "text": "Om andere leden in jouw omgeving te vinden, voer nu je locatie in op de kaart!"
}, }
"info": "Zo gaat dat"
}, },
"circles": { "circles": {
"button": "Cirkels openen...",
"headline": "Samen ondersteunen we elkaar - mindful in de cirkelcultuur.", "headline": "Samen ondersteunen we elkaar - mindful in de cirkelcultuur.",
"text": "Klik op de knop om het samenwerkingsplatform te openen in een nieuw browservenster.", "text": "Klik op de knop om het samenwerkingsplatform te openen in een nieuw browservenster."
"button": "Cirkels openen..."
}, },
"community": { "community": {
"admins": "Beheerders", "admins": "Beheerders",
@ -60,8 +60,8 @@
"moderators": "Moderators", "moderators": "Moderators",
"myContributions": "Mijn bijdragen voor het algemeen belang", "myContributions": "Mijn bijdragen voor het algemeen belang",
"noOpenContributionLinkText": "Er zijn momenteel geen link-gegenereerde creaties.", "noOpenContributionLinkText": "Er zijn momenteel geen link-gegenereerde creaties.",
"openContributionLinks": "Creaties gegenereerd door link",
"openContributionLinkText": "Voor startcredits of soortgelijke doeleinden kan de community zogenaamde creatielinks maken. Deze activeren automatische creaties die worden gecrediteerd aan de gebruiker.\nDe community \"{name}\" ondersteunt momenteel {count} link-gegenereerde creaties:", "openContributionLinkText": "Voor startcredits of soortgelijke doeleinden kan de community zogenaamde creatielinks maken. Deze activeren automatische creaties die worden gecrediteerd aan de gebruiker.\nDe community \"{name}\" ondersteunt momenteel {count} link-gegenereerde creaties:",
"openContributionLinks": "Creaties gegenereerd door link",
"other-communities": "Verdere gemeenschappen", "other-communities": "Verdere gemeenschappen",
"startNewsButton": "Stuur Gradidos", "startNewsButton": "Stuur Gradidos",
"statistic": "Statistieken", "statistic": "Statistieken",
@ -103,6 +103,7 @@
}, },
"copy-to-clipboard": "Kopieer naar klembord", "copy-to-clipboard": "Kopieer naar klembord",
"decay": { "decay": {
"Starting_block_decay": "Startblok vergankelijkheid",
"before_startblock_transaction": "Deze transactie heeft geen vergankelijkheid.", "before_startblock_transaction": "Deze transactie heeft geen vergankelijkheid.",
"calculation_decay": "Berekening van de vergankelijkheid", "calculation_decay": "Berekening van de vergankelijkheid",
"calculation_total": "Berekening van het totaalbedrag", "calculation_total": "Berekening van het totaalbedrag",
@ -111,7 +112,6 @@
"decay_since_last_transaction": "Vergankelijkheid sinds de laatste transactie", "decay_since_last_transaction": "Vergankelijkheid sinds de laatste transactie",
"last_transaction": "Laatste transactie", "last_transaction": "Laatste transactie",
"past_time": "Verlopen tijd", "past_time": "Verlopen tijd",
"Starting_block_decay": "Startblok vergankelijkheid",
"total": "Totaal", "total": "Totaal",
"types": { "types": {
"creation": "Gecreëerd", "creation": "Gecreëerd",
@ -166,8 +166,8 @@
"link_decay_description": "Het linkbedrag wordt geblokkeerd samen met de maximale vergankelijkheid. Nadat de link is ingewisseld, verlopen of verwijderd, wordt het resterende bedrag weer vrijgegeven.", "link_decay_description": "Het linkbedrag wordt geblokkeerd samen met de maximale vergankelijkheid. Nadat de link is ingewisseld, verlopen of verwijderd, wordt het resterende bedrag weer vrijgegeven.",
"memo": "Memo", "memo": "Memo",
"message": "Bericht", "message": "Bericht",
"new_balance": "Nieuw banksaldo na bevestiging",
"newPasswordRepeat": "Nieuw wachtwoord herhalen", "newPasswordRepeat": "Nieuw wachtwoord herhalen",
"new_balance": "Nieuw banksaldo na bevestiging",
"no_gdd_available": "Je hebt geen GDD om te versturen.", "no_gdd_available": "Je hebt geen GDD om te versturen.",
"password": "Wachtwoord", "password": "Wachtwoord",
"passwordRepeat": "Wachtwoord herhalen", "passwordRepeat": "Wachtwoord herhalen",
@ -179,11 +179,11 @@
"reset": "Resetten", "reset": "Resetten",
"save": "Opslaan", "save": "Opslaan",
"scann_code": "<strong>QR Code Scanner</strong> - Scan de QR Code van uw partner", "scann_code": "<strong>QR Code Scanner</strong> - Scan de QR Code van uw partner",
"sender": "Afzender",
"send_check": "Bevestig jouw transactie. Controleer alsjeblieft nogmaals alle gegevens!", "send_check": "Bevestig jouw transactie. Controleer alsjeblieft nogmaals alle gegevens!",
"send_now": "Nu versturen", "send_now": "Nu versturen",
"send_transaction_error": "Helaas kon de transactie niet uitgevoerd worden!", "send_transaction_error": "Helaas kon de transactie niet uitgevoerd worden!",
"send_transaction_success": " Jouw transactie werd succesvol uitgevoerd ", "send_transaction_success": " Jouw transactie werd succesvol uitgevoerd ",
"sender": "Afzender",
"sorry": "Sorry", "sorry": "Sorry",
"thx": "Dankjewel", "thx": "Dankjewel",
"time": "Tijd", "time": "Tijd",
@ -295,6 +295,7 @@
}, },
"openHours": "Open Hours", "openHours": "Open Hours",
"pageTitle": { "pageTitle": {
"circles": "Gradido Kringen",
"contributions": "Gradido scoop", "contributions": "Gradido scoop",
"gdt": "Your GDT transactions", "gdt": "Your GDT transactions",
"information": "{community}", "information": "{community}",
@ -302,7 +303,6 @@
"send": "Send Gradidos", "send": "Send Gradidos",
"settings": "Settings", "settings": "Settings",
"transactions": "Your transactions", "transactions": "Your transactions",
"circles": "Gradido Kringen",
"usersearch": "Geografisch leden zoeken (bèta)" "usersearch": "Geografisch leden zoeken (bèta)"
}, },
"qrCode": "QR Code", "qrCode": "QR Code",
@ -326,10 +326,10 @@
"communityCoords": "Locatie van je gemeenschap: Lat {lat}, Lng {lng}", "communityCoords": "Locatie van je gemeenschap: Lat {lat}, Lng {lng}",
"communityLocationLabel": "Locatie van je gemeenschap", "communityLocationLabel": "Locatie van je gemeenschap",
"headline": "Geografische locatiebepaling van de gebruiker", "headline": "Geografische locatiebepaling van de gebruiker",
"userCoords": "Jouw locatie: Lat {lat}, Lng {lng}",
"userLocationLabel": "Jouw locatie",
"search": "Zoek een locatie", "search": "Zoek een locatie",
"success": "Locatie succesvol opgeslagen" "success": "Locatie succesvol opgeslagen",
"userCoords": "Jouw locatie: Lat {lat}, Lng {lng}",
"userLocationLabel": "Jouw locatie"
} }
}, },
"language": { "language": {
@ -414,9 +414,9 @@
"send_you": "stuurt jou" "send_you": "stuurt jou"
}, },
"usersearch": { "usersearch": {
"button": "Start het zoeken naar gebruikers...",
"headline": "Geografisch zoeken naar gebruikers", "headline": "Geografisch zoeken naar gebruikers",
"text": "Het maakt niet uit tot welke community je behoort, met het Geo Matching System kun je leden van alle communities vinden op een kaart. Je kunt filteren op aanbiedingen en vereisten en de gebruikers weergeven die aan je profiel voldoen.\n\nDe knop opent een nieuw browservenster waarin de gebruikers in je omgeving op een kaart worden weergegeven.", "text": "Het maakt niet uit tot welke community je behoort, met het Geo Matching System kun je leden van alle communities vinden op een kaart. Je kunt filteren op aanbiedingen en vereisten en de gebruikers weergeven die aan je profiel voldoen.\n\nDe knop opent een nieuw browservenster waarin de gebruikers in je omgeving op een kaart worden weergegeven."
"button": "Start het zoeken naar gebruikers..."
}, },
"via_link": "via een link", "via_link": "via een link",
"welcome": "Welkom in de gemeenschap" "welcome": "Welkom in de gemeenschap"

View File

@ -1,8 +1,8 @@
{ {
"100": "%100",
"1000thanks": "Bizimle olduğun için 1000lerce teşekkür!",
"125": "%125",
"85": "%85", "85": "%85",
"100": "%100",
"125": "%125",
"1000thanks": "Bizimle olduğun için 1000lerce teşekkür!",
"GDD": "GDD", "GDD": "GDD",
"GDT": "GDT", "GDT": "GDT",
"PersonalDetails": "Kişisel bilgiler", "PersonalDetails": "Kişisel bilgiler",
@ -64,6 +64,7 @@
"thanksYouWith": "Sana gönderiyor" "thanksYouWith": "Sana gönderiyor"
}, },
"decay": { "decay": {
"Starting_block_decay": "Blok erimenin başlatılması ",
"before_startblock_transaction": "Bu işlem erimeyi içermez.", "before_startblock_transaction": "Bu işlem erimeyi içermez.",
"calculation_decay": "Erimenin hesaplanması", "calculation_decay": "Erimenin hesaplanması",
"calculation_total": "Toplam miktarın hesaplanması", "calculation_total": "Toplam miktarın hesaplanması",
@ -72,7 +73,6 @@
"decay_since_last_transaction": "Son işlemden bu yana olan erime", "decay_since_last_transaction": "Son işlemden bu yana olan erime",
"last_transaction": "Son işlem:", "last_transaction": "Son işlem:",
"past_time": "Geçen süre", "past_time": "Geçen süre",
"Starting_block_decay": "Blok erimenin başlatılması ",
"total": "Toplam", "total": "Toplam",
"types": { "types": {
"creation": "Oluşturuldu", "creation": "Oluşturuldu",
@ -123,8 +123,8 @@
"lastname": "Soyadı", "lastname": "Soyadı",
"memo": "Mesaj", "memo": "Mesaj",
"message": "Mesaj", "message": "Mesaj",
"new_balance": "Onay sonrası hesap bakiyesi",
"newPasswordRepeat": "Yeni şifreyi tekrarla", "newPasswordRepeat": "Yeni şifreyi tekrarla",
"new_balance": "Onay sonrası hesap bakiyesi",
"no_gdd_available": "Göndermek için GDD'niz yok.", "no_gdd_available": "Göndermek için GDD'niz yok.",
"password": "Şifre", "password": "Şifre",
"passwordRepeat": "Şifreyi tekrarla", "passwordRepeat": "Şifreyi tekrarla",
@ -135,11 +135,11 @@
"reset": "Sıfırla", "reset": "Sıfırla",
"save": "Kaydet", "save": "Kaydet",
"scann_code": "<strong>QR Code Scanner</strong> - Ortağınızın QR Kodunu tarayın", "scann_code": "<strong>QR Code Scanner</strong> - Ortağınızın QR Kodunu tarayın",
"sender": "Gönderen",
"send_check": "İşlemi onayla. Lütfen tüm verileri tekrar kontrol et!", "send_check": "İşlemi onayla. Lütfen tüm verileri tekrar kontrol et!",
"send_now": "Şimdi gönder", "send_now": "Şimdi gönder",
"send_transaction_error": "Ne yazık ki işlem gerçekleştirilemedi!", "send_transaction_error": "Ne yazık ki işlem gerçekleştirilemedi!",
"send_transaction_success": "İşleminiz başarıyla tamamlandı", "send_transaction_success": "İşleminiz başarıyla tamamlandı",
"sender": "Gönderen",
"sorry": "Üzgünüz", "sorry": "Üzgünüz",
"thx": "Teşekkür ederiz", "thx": "Teşekkür ederiz",
"to": "Geçerlik", "to": "Geçerlik",
@ -257,10 +257,10 @@
"communityCoords": "Topluluk Konumunuz: Enlem {lat}, Boylam {lng}", "communityCoords": "Topluluk Konumunuz: Enlem {lat}, Boylam {lng}",
"communityLocationLabel": "Topluluk Konumunuz", "communityLocationLabel": "Topluluk Konumunuz",
"headline": "Kullanıcının Coğrafi Konum Tespiti", "headline": "Kullanıcının Coğrafi Konum Tespiti",
"userCoords": "Konumunuz: Enlem {lat}, Boylam {lng}",
"userLocationLabel": "Konumunuz",
"search": "Konum ara", "search": "Konum ara",
"success": "Konum başarıyla kaydedildi" "success": "Konum başarıyla kaydedildi",
"userCoords": "Konumunuz: Enlem {lat}, Boylam {lng}",
"userLocationLabel": "Konumunuz"
} }
}, },
"language": { "language": {

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido", "name": "gradido",
"version": "2.7.3", "version": "2.7.4",
"description": "Gradido", "description": "Gradido",
"main": "index.js", "main": "index.js",
"repository": "git@github.com:gradido/gradido.git", "repository": "git@github.com:gradido/gradido.git",
@ -21,7 +21,8 @@
], ],
"scripts": { "scripts": {
"release": "bumpp -r", "release": "bumpp -r",
"version": "auto-changelog -p --commit-limit 0 && git add CHANGELOG.md", "version": "auto-changelog -p --commit-limit 0 && git add CHANGELOG.md && git commit -m \"update changelog\" && git push",
"postversion": "git push origin :refs/tags/latest && git tag -f latest && git push origin latest",
"installAll": "bun run install", "installAll": "bun run install",
"docker": "cross-env BUILD_COMMIT=$(git rev-parse HEAD) docker compose -f docker-compose.yml up", "docker": "cross-env BUILD_COMMIT=$(git rev-parse HEAD) docker compose -f docker-compose.yml up",
"docker:rebuild": "cross-env BUILD_COMMIT=$(git rev-parse HEAD) docker compose -f docker-compose.yml build", "docker:rebuild": "cross-env BUILD_COMMIT=$(git rev-parse HEAD) docker compose -f docker-compose.yml build",

View File

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

View File

@ -4,6 +4,7 @@ export * from './helper'
export * from './jwt/JWT' export * from './jwt/JWT'
export * from './jwt/payloadtypes/AuthenticationJwtPayloadType' export * from './jwt/payloadtypes/AuthenticationJwtPayloadType'
export * from './jwt/payloadtypes/AuthenticationResponseJwtPayloadType' export * from './jwt/payloadtypes/AuthenticationResponseJwtPayloadType'
export * from './jwt/payloadtypes/CommandJwtPayloadType'
export * from './jwt/payloadtypes/DisburseJwtPayloadType' export * from './jwt/payloadtypes/DisburseJwtPayloadType'
export * from './jwt/payloadtypes/EncryptedJWEJwtPayloadType' export * from './jwt/payloadtypes/EncryptedJWEJwtPayloadType'
export * from './jwt/payloadtypes/JwtPayloadType' export * from './jwt/payloadtypes/JwtPayloadType'

View File

@ -0,0 +1,22 @@
import { JwtPayloadType } from './JwtPayloadType'
export class CommandJwtPayloadType extends JwtPayloadType {
static COMMAND_TYPE = 'command'
commandName: string
commandClass: string
commandArgs: string[]
constructor(
handshakeID: string,
commandName: string,
commandClass: string,
commandArgs: string[],
) {
super(handshakeID)
this.tokentype = CommandJwtPayloadType.COMMAND_TYPE
this.commandName = commandName
this.commandClass = commandClass
this.commandArgs = commandArgs
}
}