From 0c10a3117833d1fa328e5d14f7547b16bcbcc105 Mon Sep 17 00:00:00 2001
From: clauspeterhuebner
Date: Wed, 10 Sep 2025 00:53:08 +0200
Subject: [PATCH 01/25] adapt first part of redeem UI requirements
---
.../RedeemCommunitySelection.vue | 118 +++++++------
.../RedeemSelectCommunity.vue | 58 +++----
frontend/src/locales/de.json | 8 +-
frontend/src/locales/en.json | 8 +-
frontend/src/locales/es.json | 3 +
frontend/src/locales/fr.json | 3 +
frontend/src/locales/nl.json | 3 +
frontend/src/pages/TransactionLink.vue | 164 ++++++++++--------
8 files changed, 197 insertions(+), 168 deletions(-)
diff --git a/frontend/src/components/LinkInformations/RedeemCommunitySelection.vue b/frontend/src/components/LinkInformations/RedeemCommunitySelection.vue
index 46e9eb81f..cb17e3bf5 100644
--- a/frontend/src/components/LinkInformations/RedeemCommunitySelection.vue
+++ b/frontend/src/components/LinkInformations/RedeemCommunitySelection.vue
@@ -6,15 +6,33 @@
:is-redeem-jwt-link="isRedeemJwtLink"
class="redeem-community-selection"
>
-
+
{{ $t('gdd_per_link.redeemlink-error') }}
+
+ {{ linkData.senderUser.firstName }}
+ {{ $t('transaction-link.send_you') }} {{ linkData.amount }} {{ $t('GDD-long') }}
+
+
+
+
+ {{ linkData.memo }}
+
+
+
-
- {{ $t('gdd_per_link.recipientCommunitySelection') }}
+
+
+ {{ $t('gdd_per_link.recipientCommunityRedirection') }}
+
+
+ {{ $t('gdd_per_link.recipientCommunitySelection') }}
+
+
+
+ {{ $t('gdd_per_link.recipientCommunityFix') }}
- {{ $t('gdd_per_link.recipientCommunityFix') }}
@@ -29,7 +47,6 @@
{{ currentRecipientCommunity.name }}
- {{ $t('gdd_per_link.switchCommunity') }}
{{ $t('gdd_per_link.to-switch') }}
@@ -37,13 +54,8 @@
-
- {{ linkData.senderUser.firstName }}
- {{ $t('transaction-link.send_you') }} {{ $filters.GDD(linkData.amount) }}
-
-
- {{ linkData.memo }}
-
+
+
@@ -83,6 +113,10 @@ export default {
font-size: 2.25rem;
}
+.navbar-like-link {
+ color: rgba(var(--bs-link-color-rgb));
+}
+
button.navbar-toggler > span.navbar-toggler-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(4, 112, 6, 1)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
}
diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json
index ec53038a3..ac9647e96 100644
--- a/frontend/src/locales/de.json
+++ b/frontend/src/locales/de.json
@@ -302,6 +302,7 @@
"raise": "Erhöhung",
"recruited-member": "Eingeladenes Mitglied"
},
+ "gradidoid-copied-to-clipboard": "Gradido ID wurde in die Zwischenablage kopiert.",
"h": "h",
"info": "Information",
"language": "Sprache",
diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json
index 2fbf4ab2a..d70bafe5c 100644
--- a/frontend/src/locales/en.json
+++ b/frontend/src/locales/en.json
@@ -302,6 +302,7 @@
"raise": "Increase",
"recruited-member": "Invited member"
},
+ "gradidoid-copied-to-clipboard": "Gradido ID copied to clipboard.",
"h": "h",
"info": "Information",
"language": "Language",
diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json
index c4c77bbaa..19a3accfd 100644
--- a/frontend/src/locales/es.json
+++ b/frontend/src/locales/es.json
@@ -251,6 +251,7 @@
"raise": "Aumento",
"recruited-member": "Miembro invitado"
},
+ "gradidoid-copied-to-clipboard": "Gradido ID copiado al portapapeles.",
"info": "Información",
"language": "Idioma",
"link-load": "recargar el último enlace | recargar los últimos {n} enlaces",
diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json
index b531274a0..a24a4c7aa 100644
--- a/frontend/src/locales/fr.json
+++ b/frontend/src/locales/fr.json
@@ -192,9 +192,7 @@
"gddCreationTime": "Le champ {_field_} doit comprendre un nombre entre {min} et {max} avec un maximum de une décimale.",
"gddSendAmount": "Le champ {_field_} doit comprendre un nombre entre {min} et {max} avec un maximum de deux chiffres après la virgule",
"identifier": {
- "communityIsReachable": "Communauté non joignable!",
- "communityIsReachable.communityNotFound": "Communauté non trouvée!",
- "communityIsReachable.communityNotReachable": "Communauté non joignable!",
+ "communityIsReachable": "Communauté non trouvée ou non joignable!",
"required": "Le destinataire est un champ obligatoire.",
"typeError": "Le destinataire doit être un email, un nom d'utilisateur ou un Gradido ID."
},
@@ -260,6 +258,7 @@
"raise": "Augmentation",
"recruited-member": "Membre invité"
},
+ "gradidoid-copied-to-clipboard": "Gradido ID copié dans le presse-papier.",
"h": "h",
"info": "Information",
"language": "Langage",
diff --git a/frontend/src/locales/nl.json b/frontend/src/locales/nl.json
index 6f78583e5..587dec716 100644
--- a/frontend/src/locales/nl.json
+++ b/frontend/src/locales/nl.json
@@ -251,6 +251,7 @@
"raise": "Verhoging",
"recruited-member": "Uitgenodigd lid"
},
+ "gradidoid-copied-to-clipboard": "Gradido ID gekopieerd naar klembord.",
"info": "Informatie",
"language": "Taal",
"link-load": "de laatste link herladen | de laatste links herladen",
diff --git a/frontend/vite.config.mjs b/frontend/vite.config.mjs
index 2296f86ba..4a0674b0f 100644
--- a/frontend/vite.config.mjs
+++ b/frontend/vite.config.mjs
@@ -125,7 +125,7 @@ export default defineConfig(async ({ command }) => {
GRAPHQL_URI: CONFIG.GRAPHQL_URI, // null,
ADMIN_AUTH_PATH: CONFIG.ADMIN_AUTH_PATH ?? null, // it is the only env without exported default
ADMIN_AUTH_URL: CONFIG.ADMIN_AUTH_URL, // null,
- COMMUNITY_NAME: null,
+ COMMUNITY_NAME: CONFIG.COMMUNITY_NAME,
COMMUNITY_REGISTER_PATH: null,
COMMUNITY_REGISTER_URL: null,
COMMUNITY_DESCRIPTION: null,
From 28fcd4988d8958c4a84a9146833929350cfc3418 Mon Sep 17 00:00:00 2001
From: einhornimmond
Date: Wed, 1 Oct 2025 12:36:30 +0200
Subject: [PATCH 21/25] fix one small graphql definition bug
---
.../src/graphql/api/1_0/resolver/AuthenticationResolver.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts
index 3c62c7bef..73eb70d26 100644
--- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts
+++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts
@@ -109,7 +109,7 @@ export class AuthenticationResolver {
}
}
- @Mutation(() => String)
+ @Mutation(() => String, { nullable: true })
async authenticate(
@Arg('data')
args: EncryptedTransferArgs,
From f8ef8e111eb596257ac60fb2e37cc4a336483be4 Mon Sep 17 00:00:00 2001
From: einhornimmond
Date: Wed, 1 Oct 2025 13:49:10 +0200
Subject: [PATCH 22/25] log bug fix. use federated_communities.verified_at
instead of communities.authenticated_at date for deciding reachable
communities
---
.../resolver/CommunityResolver.test.ts | 50 ++++++++++---------
.../src/log4js-config/coloredContext.ts | 2 +-
database/src/queries/communities.test.ts | 46 +++++++++++++----
database/src/queries/communities.ts | 9 +++-
database/src/seeds/community.ts | 24 ++++++---
5 files changed, 86 insertions(+), 45 deletions(-)
diff --git a/backend/src/graphql/resolver/CommunityResolver.test.ts b/backend/src/graphql/resolver/CommunityResolver.test.ts
index b65cb65c0..5e7d929e2 100644
--- a/backend/src/graphql/resolver/CommunityResolver.test.ts
+++ b/backend/src/graphql/resolver/CommunityResolver.test.ts
@@ -16,10 +16,9 @@ import {
reachableCommunities,
} from '@/seeds/graphql/queries'
import { peterLustig } from '@/seeds/users/peter-lustig'
-import { createCommunity, createAuthenticatedForeignCommunity } from 'database/src/seeds/community'
+import { createCommunity, createVerifiedFederatedCommunity } from 'database/src/seeds/community'
import { getLogger } from 'config-schema/test/testSetup'
-import { getCommunityByUuid } from './util/communities'
import { CONFIG } from '@/config'
jest.mock('@/password/EncryptorUtils')
@@ -445,6 +444,7 @@ describe('CommunityResolver', () => {
afterAll(async () => {
await DbCommunity.clear()
+ await DbFederatedCommunity.clear()
})
describe('with empty list', () => {
@@ -483,33 +483,37 @@ describe('CommunityResolver', () => {
describe('returns 2 filtered communities even with 3 existing entries', () => {
beforeEach(async () => {
foreignCom1 = await createCommunity(true, false)
- foreignCom2 = await createAuthenticatedForeignCommunity(100, false)
+ foreignCom2 = await createCommunity(true, false)
+ const com1FedCom = await createVerifiedFederatedCommunity('1_0', 100, foreignCom1, false)
+ const com1FedCom2 = await createVerifiedFederatedCommunity('1_1', 100, foreignCom1, false)
+ const com2FedCom = await createVerifiedFederatedCommunity('1_0', 10000, foreignCom2, false)
await Promise.all([
DbCommunity.insert(foreignCom1),
- DbCommunity.insert(foreignCom2)
+ DbCommunity.insert(foreignCom2),
+ DbFederatedCommunity.insert(com1FedCom),
+ DbFederatedCommunity.insert(com1FedCom2),
+ DbFederatedCommunity.insert(com2FedCom)
])
})
it('returns 2 community entries', async () => {
- await expect(query({ query: reachableCommunities })).resolves.toMatchObject({
- data: {
- reachableCommunities: [
- {
- foreign: homeCom1.foreign,
- name: homeCom1.name,
- description: homeCom1.description,
- url: homeCom1.url,
- uuid: homeCom1.communityUuid,
- }, {
- foreign: foreignCom2.foreign,
- name: foreignCom2.name,
- description: foreignCom2.description,
- url: foreignCom2.url,
- uuid: foreignCom2.communityUuid,
- },
- ],
- },
- })
+ const result = await query({ query: reachableCommunities })
+ expect(result.data.reachableCommunities.length).toBe(2)
+ expect(result.data.reachableCommunities).toMatchObject([
+ {
+ foreign: homeCom1.foreign,
+ name: homeCom1.name,
+ description: homeCom1.description,
+ url: homeCom1.url,
+ uuid: homeCom1.communityUuid,
+ }, {
+ foreign: foreignCom1.foreign,
+ name: foreignCom1.name,
+ description: foreignCom1.description,
+ url: foreignCom1.url,
+ uuid: foreignCom1.communityUuid,
+ }
+ ])
})
})
diff --git a/config-schema/src/log4js-config/coloredContext.ts b/config-schema/src/log4js-config/coloredContext.ts
index 1ebb6b219..1f238fcdb 100644
--- a/config-schema/src/log4js-config/coloredContext.ts
+++ b/config-schema/src/log4js-config/coloredContext.ts
@@ -33,7 +33,7 @@ function composeDataString(data: (string | Object)[]): string {
return data
.map((d) => {
// if it is a object and his toString function return only garbage
- if (typeof d === 'object' && d.toString() === '[object Object]') {
+ if (d && typeof d === 'object' && d.toString() === '[object Object]') {
return inspect(d, )
}
if (d) {
diff --git a/database/src/queries/communities.test.ts b/database/src/queries/communities.test.ts
index 62dedabf1..18975256c 100644
--- a/database/src/queries/communities.test.ts
+++ b/database/src/queries/communities.test.ts
@@ -1,8 +1,8 @@
-import { Community as DbCommunity } from '..'
+import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from '..'
import { AppDatabase } from '../AppDatabase'
import { getHomeCommunity, getReachableCommunities } from './communities'
import { describe, expect, it, beforeEach, beforeAll, afterAll } from 'vitest'
-import { createCommunity, createAuthenticatedForeignCommunity } from '../seeds/community'
+import { createCommunity, createVerifiedFederatedCommunity } from '../seeds/community'
const db = AppDatabase.getInstance()
@@ -17,6 +17,7 @@ describe('community.queries', () => {
// clean db for every test case
beforeEach(async () => {
await DbCommunity.clear()
+ await DbFederatedCommunity.clear()
})
describe('getHomeCommunity', () => {
it('should return null if no home community exists', async () => {
@@ -44,21 +45,44 @@ describe('community.queries', () => {
expect(await getReachableCommunities(1000)).toHaveLength(1)
})
it('foreign communities authenticated within chosen range', async () => {
- await createAuthenticatedForeignCommunity(400)
- await createAuthenticatedForeignCommunity(500)
- await createAuthenticatedForeignCommunity(1200)
+ const com1 = await createCommunity(true)
+ const com2 = await createCommunity(true)
+ const com3 = await createCommunity(true)
+ await createVerifiedFederatedCommunity('1_0', 100, com1)
+ await createVerifiedFederatedCommunity('1_0', 500, com2)
+ // outside of range
+ await createVerifiedFederatedCommunity('1_0', 1200, com3)
- const community = await getReachableCommunities(1000)
- expect(community).toHaveLength(2)
+ const communities = await getReachableCommunities(1000)
+ expect(communities).toHaveLength(2)
+ expect(communities[0].communityUuid).toBe(com1.communityUuid)
+ expect(communities[1].communityUuid).toBe(com2.communityUuid)
+ })
+ it('multiple federated community api version, result in one community', async () => {
+ const com1 = await createCommunity(true)
+ await createVerifiedFederatedCommunity('1_0', 100, com1)
+ await createVerifiedFederatedCommunity('1_1', 100, com1)
+ expect(await getReachableCommunities(1000)).toHaveLength(1)
+ })
+ it('multiple federated community api version one outside of range, result in one community', async () => {
+ const com1 = await createCommunity(true)
+ await createVerifiedFederatedCommunity('1_0', 100, com1)
+ // outside of range
+ await createVerifiedFederatedCommunity('1_1', 1200, com1)
+ expect(await getReachableCommunities(1000)).toHaveLength(1)
})
it('foreign and home community', async () => {
+ // home community
await createCommunity(false)
- await createAuthenticatedForeignCommunity(400)
- await createAuthenticatedForeignCommunity(1200)
+ const com1 = await createCommunity(true)
+ const com2 = await createCommunity(true)
+ await createVerifiedFederatedCommunity('1_0', 400, com1)
+ await createVerifiedFederatedCommunity('1_0', 1200, com2)
expect(await getReachableCommunities(1000)).toHaveLength(2)
})
- it('not authenticated foreign community', async () => {
- await createCommunity(true)
+ it('not verified inside time frame federated community', async () => {
+ const com1 = await createCommunity(true)
+ await createVerifiedFederatedCommunity('1_0', 1200, com1)
expect(await getReachableCommunities(1000)).toHaveLength(0)
})
})
diff --git a/database/src/queries/communities.ts b/database/src/queries/communities.ts
index 5bf8798c8..e216f8af6 100644
--- a/database/src/queries/communities.ts
+++ b/database/src/queries/communities.ts
@@ -43,14 +43,19 @@ export async function getCommunityWithFederatedCommunityByIdentifier(
}
// returns all reachable communities
-// home community and all foreign communities which have been authenticated within the last authenticationTimeoutMs
+// home community and all federated communities which have been verified within the last authenticationTimeoutMs
export async function getReachableCommunities(
authenticationTimeoutMs: number,
order?: FindOptionsOrder
): Promise {
return await DbCommunity.find({
where: [
- { communityUuid: Not(IsNull()), authenticatedAt: MoreThanOrEqual(new Date(Date.now() - authenticationTimeoutMs)) },
+ {
+ authenticatedAt: Not(IsNull()),
+ federatedCommunities: {
+ verifiedAt: MoreThanOrEqual(new Date(Date.now() - authenticationTimeoutMs))
+ }
+ },
{ foreign: false },
],
order,
diff --git a/database/src/seeds/community.ts b/database/src/seeds/community.ts
index b57e839e1..4db872398 100644
--- a/database/src/seeds/community.ts
+++ b/database/src/seeds/community.ts
@@ -1,4 +1,4 @@
-import { Community } from '../entity'
+import { Community, FederatedCommunity } from '../entity'
import { randomBytes } from 'node:crypto'
import { v4 as uuidv4 } from 'uuid'
@@ -14,8 +14,10 @@ export async function createCommunity(foreign: boolean, save: boolean = true): P
community.name = 'ForeignCommunity-name'
community.description = 'ForeignCommunity-description'
community.url = `http://foreign-${Math.random()}/api`
+ community.authenticatedAt = new Date()
} else {
community.foreign = false
+ // todo: generate valid public/private key pair (ed25519)
community.privateKey = randomBytes(64)
community.name = 'HomeCommunity-name'
community.description = 'HomeCommunity-description'
@@ -24,11 +26,17 @@ export async function createCommunity(foreign: boolean, save: boolean = true): P
return save ? await community.save() : community
}
-export async function createAuthenticatedForeignCommunity(
- authenticatedBeforeMs: number,
+export async function createVerifiedFederatedCommunity(
+ apiVersion: string,
+ verifiedBeforeMs: number,
+ community: Community,
save: boolean = true
-): Promise {
- const foreignCom = await createCommunity(true, false)
- foreignCom.authenticatedAt = new Date(Date.now() - authenticatedBeforeMs)
- return save ? await foreignCom.save() : foreignCom
-}
\ No newline at end of file
+): Promise {
+ const federatedCommunity = new FederatedCommunity()
+ federatedCommunity.apiVersion = apiVersion
+ federatedCommunity.endPoint = community.url
+ federatedCommunity.publicKey = community.publicKey
+ federatedCommunity.community = community
+ federatedCommunity.verifiedAt = new Date(Date.now() - verifiedBeforeMs)
+ return save ? await federatedCommunity.save() : federatedCommunity
+}
From bff160a904613fb5f6132aedd72b90383d4e7dcc Mon Sep 17 00:00:00 2001
From: einhornimmond
Date: Wed, 1 Oct 2025 14:33:05 +0200
Subject: [PATCH 23/25] remove side effects from validator, transform
community_switch to field
---
frontend/src/components/CommunitySwitch.vue | 8 ++++-
.../components/GddSend/TransactionForm.vue | 32 +++++++++++++++----
2 files changed, 32 insertions(+), 8 deletions(-)
diff --git a/frontend/src/components/CommunitySwitch.vue b/frontend/src/components/CommunitySwitch.vue
index f460efa8e..1e4fff5ff 100644
--- a/frontend/src/components/CommunitySwitch.vue
+++ b/frontend/src/components/CommunitySwitch.vue
@@ -33,6 +33,10 @@ const props = defineProps({
type: Object,
default: () => ({}),
},
+ communityIdentifier: {
+ type: String,
+ default: '',
+ },
})
const emit = defineEmits(['update:modelValue', 'communitiesLoaded'])
@@ -57,7 +61,9 @@ onResult(({ data }) => {
}
})
-const communityIdentifier = computed(() => route.params.communityIdentifier)
+const communityIdentifier = computed(
+ () => route.params.communityIdentifier || props.communityIdentifier,
+)
function updateCommunity(community) {
// console.log('CommunitySwitch.updateCommunity...community=', community)
diff --git a/frontend/src/components/GddSend/TransactionForm.vue b/frontend/src/components/GddSend/TransactionForm.vue
index b66b39ed1..e14beb812 100644
--- a/frontend/src/components/GddSend/TransactionForm.vue
+++ b/frontend/src/components/GddSend/TransactionForm.vue
@@ -48,8 +48,9 @@
@@ -173,6 +174,7 @@ const entityDataToForm = computed(() => ({ ...props }))
const form = reactive({ ...entityDataToForm.value })
const disableSmartValidState = ref(false)
const communities = ref([])
+const autoCommunityIdentifier = ref('')
const emit = defineEmits(['set-transaction'])
@@ -220,6 +222,7 @@ const validationSchema = computed(() => {
return object({
memo: memoSchema,
amount: amountSchema,
+ // todo: found a better way, because this validation test has side effects
identifier: identifierSchema.test(
'community-is-reachable',
'form.validation.identifier.communityIsReachable',
@@ -229,18 +232,13 @@ const validationSchema = computed(() => {
if (parts.length !== 2) {
return true
}
- const com = communities.value.find((community) => {
+ return communities.value.some((community) => {
return (
community.uuid === parts[0] ||
community.name === parts[0] ||
community.url === parts[0]
)
})
- if (com) {
- form.targetCommunity = com
- return true
- }
- return false
},
),
})
@@ -286,6 +284,26 @@ watch(userError, (error) => {
}
})
+// if identifier contain valid community identifier of a reachable community:
+// set it as target community and change community-switch to show only current value, instead of select
+watch(
+ () => form.identifier,
+ (value) => {
+ autoCommunityIdentifier.value = ''
+ const parts = value.split('/')
+ if (parts.length === 2) {
+ const com = communities.value.find(
+ (community) =>
+ community.uuid === parts[0] || community.name === parts[0] || community.url === parts[0],
+ )
+ if (com) {
+ form.targetCommunity = com
+ autoCommunityIdentifier.value = com.uuid
+ }
+ }
+ },
+)
+
function onSubmit() {
const transformedForm = validationSchema.value.cast(form)
const parts = transformedForm.identifier.split('/')
From 84ebba4070892dc11f09df289f5080e721139b3b Mon Sep 17 00:00:00 2001
From: einhornimmond
Date: Wed, 1 Oct 2025 14:40:34 +0200
Subject: [PATCH 24/25] add debug log
---
frontend/src/components/CommunitySwitch.vue | 9 ++++++++-
frontend/src/components/GddSend/TransactionForm.vue | 7 +++++++
2 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/frontend/src/components/CommunitySwitch.vue b/frontend/src/components/CommunitySwitch.vue
index 1e4fff5ff..ed1292a7c 100644
--- a/frontend/src/components/CommunitySwitch.vue
+++ b/frontend/src/components/CommunitySwitch.vue
@@ -22,7 +22,7 @@