Merge branch 'master' into 2897-incorrect-errormessage-for-wrong-contribution-link

This commit is contained in:
Alexander Friedland 2023-05-31 11:36:14 +02:00 committed by GitHub
commit 245d9ddb8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 835 additions and 170 deletions

View File

@ -4,8 +4,49 @@ 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).
#### [1.21.0](https://github.com/gradido/gradido/compare/1.20.0...1.21.0)
- feat(frontend): preserve email after login [`#2994`](https://github.com/gradido/gradido/pull/2994)
- feat(frontend): send coins via identifier [`#2989`](https://github.com/gradido/gradido/pull/2989)
- feat(backend): export user events to klicktipp [`#2916`](https://github.com/gradido/gradido/pull/2916)
- fix(backend): add extension pug json and css to nodemon. [`#2996`](https://github.com/gradido/gradido/pull/2996)
- feat(backend): send coins via alias [`#2988`](https://github.com/gradido/gradido/pull/2988)
- refactor(backend): replace jasonwebtoken with jose [`#2975`](https://github.com/gradido/gradido/pull/2975)
- feat(frontend): username in wallet [`#2984`](https://github.com/gradido/gradido/pull/2984)
- feat(frontend): add community to send form [`#2986`](https://github.com/gradido/gradido/pull/2986)
- fix(frontend): date fns locales [`#2983`](https://github.com/gradido/gradido/pull/2983)
- refactor(federation): federation reduce spam [`#2967`](https://github.com/gradido/gradido/pull/2967)
- refactor(federation): refactor federation clients [`#2965`](https://github.com/gradido/gradido/pull/2965)
- feat(backend): migrate transactions table for x community sendcoins [`#2917`](https://github.com/gradido/gradido/pull/2917)
- feat(backend): alias in update user info [`#2727`](https://github.com/gradido/gradido/pull/2727)
- refactor(backend): eslint comments [`#2981`](https://github.com/gradido/gradido/pull/2981)
- refactor(backend): eslint security [`#2980`](https://github.com/gradido/gradido/pull/2980)
- refactor(backend): rename klicktippSignIn to subscribe. [`#2973`](https://github.com/gradido/gradido/pull/2973)
- refactor(backend): eslint typescript strict [`#2979`](https://github.com/gradido/gradido/pull/2979)
- fix(frontend): between store problems [`#2972`](https://github.com/gradido/gradido/pull/2972)
- refactor(other): delete build folders [`#2977`](https://github.com/gradido/gradido/pull/2977)
- refactor(backend): no email in user [`#2953`](https://github.com/gradido/gradido/pull/2953)
- refactor(frontend): remove email in wallet [`#2952`](https://github.com/gradido/gradido/pull/2952)
- fix(frontend): update jest-canvas-mock version to resolve window mock problem in tests [`#2974`](https://github.com/gradido/gradido/pull/2974)
- feat(federation): federation autoreload on codechange [`#2969`](https://github.com/gradido/gradido/pull/2969)
- feat(backend): add fields to subscriber [`#2887`](https://github.com/gradido/gradido/pull/2887)
- feat(backend): x-com-2: distingue communities and communities_federation in database [`#2890`](https://github.com/gradido/gradido/pull/2890)
- feat(backend): add event for subscribe and unsubscribe [`#2886`](https://github.com/gradido/gradido/pull/2886)
- refactor(backend): eslint disable more typesafety [`#2922`](https://github.com/gradido/gradido/pull/2922)
- refactor(backend): eslint disable tests typesafer [`#2921`](https://github.com/gradido/gradido/pull/2921)
- refactor(backend): eslint disable @typescript eslint/unbound method [`#2920`](https://github.com/gradido/gradido/pull/2920)
- docs(other): removed obsolete yarn cron docu [`#2909`](https://github.com/gradido/gradido/pull/2909)
- refactor(other): finalize workflow separation and resolve mariadb and database dependencies in workflow files [`#2962`](https://github.com/gradido/gradido/pull/2962)
- refactor(workflow): align workflow naming and remove docker-compose filter from build tests [`#2894`](https://github.com/gradido/gradido/pull/2894)
- refactor(backend): eslint plugin promise + fixes [`#2830`](https://github.com/gradido/gradido/pull/2830)
- fix(backend): log stack trace included [`#2915`](https://github.com/gradido/gradido/pull/2915)
- refactor(backend): prettier refine config [`#2832`](https://github.com/gradido/gradido/pull/2832)
#### [1.20.0](https://github.com/gradido/gradido/compare/1.19.1...1.20.0) #### [1.20.0](https://github.com/gradido/gradido/compare/1.19.1...1.20.0)
> 12 April 2023
- chore(release): v1.20.0 [`#2939`](https://github.com/gradido/gradido/pull/2939)
- fix(backend): no await for emails [`#2918`](https://github.com/gradido/gradido/pull/2918) - fix(backend): no await for emails [`#2918`](https://github.com/gradido/gradido/pull/2918)
- fix(frontend): no receiver on send by link [`#2933`](https://github.com/gradido/gradido/pull/2933) - fix(frontend): no receiver on send by link [`#2933`](https://github.com/gradido/gradido/pull/2933)
- fix(admin): pagination set currentPage by switch tabs [`#2902`](https://github.com/gradido/gradido/pull/2902) - fix(admin): pagination set currentPage by switch tabs [`#2902`](https://github.com/gradido/gradido/pull/2902)

View File

@ -3,7 +3,7 @@
"description": "Administraion Interface for Gradido", "description": "Administraion Interface for Gradido",
"main": "index.js", "main": "index.js",
"author": "Moriz Wahl", "author": "Moriz Wahl",
"version": "1.20.0", "version": "1.21.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"private": false, "private": false,
"scripts": { "scripts": {

View File

@ -27,7 +27,8 @@ module.exports = {
}, },
}, },
rules: { rules: {
'no-console': ['error'], 'no-console': 'error',
camelcase: ['error', { allow: ['FederationClient_*'] }],
'no-debugger': 'error', 'no-debugger': 'error',
'prettier/prettier': [ 'prettier/prettier': [
'error', 'error',
@ -184,6 +185,7 @@ module.exports = {
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
project: ['./tsconfig.json', '**/tsconfig.json'], project: ['./tsconfig.json', '**/tsconfig.json'],
// this is to properly reference the referenced project database without requirement of compiling it // this is to properly reference the referenced project database without requirement of compiling it
// eslint-disable-next-line camelcase
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true, EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
}, },
}, },

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido-backend", "name": "gradido-backend",
"version": "1.20.0", "version": "1.21.0",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions", "description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts", "main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend", "repository": "https://github.com/gradido/gradido/backend",
@ -11,11 +11,11 @@
"build": "tsc --build && mkdir -p build/src/emails/templates/ && cp -r src/emails/templates/* build/src/emails/templates/ && mkdir -p build/src/locales/ && cp -r src/locales/*.json build/src/locales/", "build": "tsc --build && mkdir -p build/src/emails/templates/ && cp -r src/emails/templates/* build/src/emails/templates/ && mkdir -p build/src/locales/ && cp -r src/locales/*.json build/src/locales/",
"clean": "tsc --build --clean", "clean": "tsc --build --clean",
"start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js", "start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js",
"dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r tsconfig-paths/register src/index.ts", "dev": "cross-env TZ=UTC nodemon -w src --ext ts,pug,json,css --exec ts-node -r tsconfig-paths/register src/index.ts",
"lint": "eslint --max-warnings=0 .", "lint": "eslint --max-warnings=0 .",
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --forceExit --detectOpenHandles", "test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --forceExit --detectOpenHandles",
"seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts", "seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts",
"klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/klicktipp.ts", "klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/executeKlicktipp.ts",
"locales": "scripts/sort.sh" "locales": "scripts/sort.sh"
}, },
"dependencies": { "dependencies": {

View File

@ -4,8 +4,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { CONFIG } from '@/config' import { CONFIG } from '@/config'
import { backendLogger as logger } from '@/server/logger'
// eslint-disable-next-line import/no-relative-parent-imports // eslint-disable-next-line import/no-relative-parent-imports
import KlicktippConnector from 'klicktipp-api' import KlicktippConnector from 'klicktipp-api'
@ -41,9 +41,12 @@ export const getKlickTippUser = async (email: string): Promise<any> => {
if (!CONFIG.KLICKTIPP) return true if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser() const isLogin = await loginKlicktippUser()
if (isLogin) { if (isLogin) {
const subscriberId = await klicktippConnector.subscriberSearch(email) try {
const result = await klicktippConnector.subscriberGet(subscriberId) return klicktippConnector.subscriberGet(await klicktippConnector.subscriberSearch(email))
return result } catch (e) {
logger.error('Could not find subscriber', email)
return false
}
} }
return false return false
} }
@ -62,8 +65,18 @@ export const addFieldsToSubscriber = async (
if (!CONFIG.KLICKTIPP) return true if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser() const isLogin = await loginKlicktippUser()
if (isLogin) { if (isLogin) {
const subscriberId = await klicktippConnector.subscriberSearch(email) try {
return klicktippConnector.subscriberUpdate(subscriberId, fields, newemail, newsmsnumber) logger.info('Updating of subscriber', email)
return klicktippConnector.subscriberUpdate(
await klicktippConnector.subscriberSearch(email),
fields,
newemail,
newsmsnumber,
)
} catch (e) {
logger.error('Could not update subscriber', email, fields, e)
return false
}
} }
return false return false
} }

View File

@ -1,11 +1,11 @@
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { GraphQLClient } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import { getPublicKey } from '@/federation/query/getPublicKey' import { getPublicKey } from '@/federation/client/1_0/query/getPublicKey'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
export class Client_1_0 { export class FederationClient {
dbCom: DbFederatedCommunity dbCom: DbFederatedCommunity
endpoint: string endpoint: string
client: GraphQLClient client: GraphQLClient

View File

@ -0,0 +1,5 @@
// eslint-disable-next-line camelcase
import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient'
// eslint-disable-next-line camelcase
export class FederationClient extends V1_0_FederationClient {}

View File

@ -1,5 +0,0 @@
// eslint-disable-next-line camelcase
import { Client_1_0 } from './Client_1_0'
// eslint-disable-next-line camelcase
export class Client_1_1 extends Client_1_0 {}

View File

@ -1,24 +1,23 @@
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
// eslint-disable-next-line camelcase
import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient'
// eslint-disable-next-line camelcase
import { FederationClient as V1_1_FederationClient } from '@/federation/client/1_1/FederationClient'
import { ApiVersionType } from '@/federation/enum/apiVersionType' import { ApiVersionType } from '@/federation/enum/apiVersionType'
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
import { Client_1_0 } from './Client_1_0' type FederationClient = V1_0_FederationClient | V1_1_FederationClient
// eslint-disable-next-line camelcase
import { Client_1_1 } from './Client_1_1'
// eslint-disable-next-line camelcase interface FederationClientInstance {
type FederationClient = Client_1_0 | Client_1_1
interface ClientInstance {
id: number id: number
// eslint-disable-next-line no-use-before-define // eslint-disable-next-line no-use-before-define
client: FederationClient client: FederationClient
} }
// eslint-disable-next-line @typescript-eslint/no-extraneous-class // eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class Client { export class FederationClientFactory {
private static instanceArray: ClientInstance[] = [] private static instanceArray: FederationClientInstance[] = []
/** /**
* The Singleton's constructor should always be private to prevent direct * The Singleton's constructor should always be private to prevent direct
@ -30,9 +29,9 @@ export class Client {
private static createFederationClient = (dbCom: DbFederatedCommunity) => { private static createFederationClient = (dbCom: DbFederatedCommunity) => {
switch (dbCom.apiVersion) { switch (dbCom.apiVersion) {
case ApiVersionType.V1_0: case ApiVersionType.V1_0:
return new Client_1_0(dbCom) return new V1_0_FederationClient(dbCom)
case ApiVersionType.V1_1: case ApiVersionType.V1_1:
return new Client_1_1(dbCom) return new V1_1_FederationClient(dbCom)
default: default:
return null return null
} }
@ -45,13 +44,18 @@ export class Client {
* just one instance of each subclass around. * just one instance of each subclass around.
*/ */
public static getInstance(dbCom: DbFederatedCommunity): FederationClient | null { public static getInstance(dbCom: DbFederatedCommunity): FederationClient | null {
const instance = Client.instanceArray.find((instance) => instance.id === dbCom.id) const instance = FederationClientFactory.instanceArray.find(
(instance) => instance.id === dbCom.id,
)
if (instance) { if (instance) {
return instance.client return instance.client
} }
const client = Client.createFederationClient(dbCom) const client = FederationClientFactory.createFederationClient(dbCom)
if (client) { if (client) {
Client.instanceArray.push({ id: dbCom.id, client } as ClientInstance) FederationClientFactory.instanceArray.push({
id: dbCom.id,
client,
} as FederationClientInstance)
} }
return client return client
} }

View File

@ -8,6 +8,8 @@
import { Connection } from '@dbTools/typeorm' import { Connection } from '@dbTools/typeorm'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { ApolloServerTestClient } from 'apollo-server-testing' import { ApolloServerTestClient } from 'apollo-server-testing'
import { GraphQLClient } from 'graphql-request'
import { Response } from 'graphql-request/dist/types'
import { testEnvironment, cleanDB } from '@test/helpers' import { testEnvironment, cleanDB } from '@test/helpers'
import { logger } from '@test/testSetup' import { logger } from '@test/testSetup'
@ -57,10 +59,23 @@ describe('validate Communities', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 0 dbCommunities`) expect(logger.debug).toBeCalledWith(`Federation: found 0 dbCommunities`)
}) })
describe('with one Community of api 1_0', () => { describe('with one Community of api 1_0 and not matching pubKey', () => {
beforeEach(async () => { beforeEach(async () => {
// eslint-disable-next-line @typescript-eslint/require-await
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return {
data: {
getPublicKey: {
publicKey: 'somePubKey',
},
},
} as Response<unknown>
})
const variables1 = { const variables1 = {
publicKey: Buffer.from('11111111111111111111111111111111'), publicKey: Buffer.from(
'1111111111111111111111111111111111111111111111111111111111111111',
),
apiVersion: '1_0', apiVersion: '1_0',
endPoint: 'http//localhost:5001/api/', endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(), lastAnnouncedAt: new Date(),
@ -70,6 +85,7 @@ describe('validate Communities', () => {
.into(DbFederatedCommunity) .into(DbFederatedCommunity)
.values(variables1) .values(variables1)
.orUpdate({ .orUpdate({
// eslint-disable-next-line camelcase
conflict_target: ['id', 'publicKey', 'apiVersion'], conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'], overwrite: ['end_point', 'last_announced_at'],
}) })
@ -88,11 +104,85 @@ describe('validate Communities', () => {
'http//localhost:5001/api/1_0/', 'http//localhost:5001/api/1_0/',
) )
}) })
it('logs not matching publicKeys', () => {
expect(logger.warn).toBeCalledWith(
'Federation: received not matching publicKey:',
'somePubKey',
expect.stringMatching('1111111111111111111111111111111111111111111111111111111111111111'),
)
})
})
describe('with one Community of api 1_0 and matching pubKey', () => {
beforeEach(async () => {
// eslint-disable-next-line @typescript-eslint/require-await
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return {
data: {
getPublicKey: {
publicKey: '1111111111111111111111111111111111111111111111111111111111111111',
},
},
} as Response<unknown>
})
const variables1 = {
publicKey: Buffer.from(
'1111111111111111111111111111111111111111111111111111111111111111',
),
apiVersion: '1_0',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbFederatedCommunity.createQueryBuilder()
.insert()
.into(DbFederatedCommunity)
.values(variables1)
.orUpdate({
// eslint-disable-next-line camelcase
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
await DbFederatedCommunity.update({}, { verifiedAt: null })
jest.clearAllMocks()
await validateCommunities()
})
it('logs one community found', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`)
})
it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith(
'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_0/',
)
})
it('logs community pubKey verified', () => {
expect(logger.info).toHaveBeenNthCalledWith(
3,
'Federation: verified community with',
'http//localhost:5001/api/',
)
})
}) })
describe('with two Communities of api 1_0 and 1_1', () => { describe('with two Communities of api 1_0 and 1_1', () => {
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks()
// eslint-disable-next-line @typescript-eslint/require-await
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return {
data: {
getPublicKey: {
publicKey: '1111111111111111111111111111111111111111111111111111111111111111',
},
},
} as Response<unknown>
})
const variables2 = { const variables2 = {
publicKey: Buffer.from('11111111111111111111111111111111'), publicKey: Buffer.from(
'1111111111111111111111111111111111111111111111111111111111111111',
),
apiVersion: '1_1', apiVersion: '1_1',
endPoint: 'http//localhost:5001/api/', endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(), lastAnnouncedAt: new Date(),
@ -102,11 +192,13 @@ describe('validate Communities', () => {
.into(DbFederatedCommunity) .into(DbFederatedCommunity)
.values(variables2) .values(variables2)
.orUpdate({ .orUpdate({
// eslint-disable-next-line camelcase
conflict_target: ['id', 'publicKey', 'apiVersion'], conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'], overwrite: ['end_point', 'last_announced_at'],
}) })
.execute() .execute()
await DbFederatedCommunity.update({}, { verifiedAt: null })
jest.clearAllMocks() jest.clearAllMocks()
await validateCommunities() await validateCommunities()
}) })
@ -130,7 +222,9 @@ describe('validate Communities', () => {
let dbCom: DbFederatedCommunity let dbCom: DbFederatedCommunity
beforeEach(async () => { beforeEach(async () => {
const variables3 = { const variables3 = {
publicKey: Buffer.from('11111111111111111111111111111111'), publicKey: Buffer.from(
'1111111111111111111111111111111111111111111111111111111111111111',
),
apiVersion: '2_0', apiVersion: '2_0',
endPoint: 'http//localhost:5001/api/', endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(), lastAnnouncedAt: new Date(),
@ -140,6 +234,7 @@ describe('validate Communities', () => {
.into(DbFederatedCommunity) .into(DbFederatedCommunity)
.values(variables3) .values(variables3)
.orUpdate({ .orUpdate({
// eslint-disable-next-line camelcase
conflict_target: ['id', 'publicKey', 'apiVersion'], conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'], overwrite: ['end_point', 'last_announced_at'],
}) })
@ -147,6 +242,7 @@ describe('validate Communities', () => {
dbCom = await DbFederatedCommunity.findOneOrFail({ dbCom = await DbFederatedCommunity.findOneOrFail({
where: { publicKey: variables3.publicKey, apiVersion: variables3.apiVersion }, where: { publicKey: variables3.publicKey, apiVersion: variables3.apiVersion },
}) })
await DbFederatedCommunity.update({}, { verifiedAt: null })
jest.clearAllMocks() jest.clearAllMocks()
await validateCommunities() await validateCommunities()
}) })

View File

@ -3,9 +3,11 @@
import { IsNull } from '@dbTools/typeorm' import { IsNull } from '@dbTools/typeorm'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
// eslint-disable-next-line camelcase
import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient'
import { FederationClientFactory } from '@/federation/client/FederationClientFactory'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
import { Client } from './client/Client'
import { ApiVersionType } from './enum/apiVersionType' import { ApiVersionType } from './enum/apiVersionType'
export function startValidateCommunities(timerInterval: number): void { export function startValidateCommunities(timerInterval: number): void {
@ -37,11 +39,13 @@ export async function validateCommunities(): Promise<void> {
continue continue
} }
try { try {
const client = Client.getInstance(dbCom) const client = FederationClientFactory.getInstance(dbCom)
const pubKey = await client?.getPublicKey() // eslint-disable-next-line camelcase
if (client instanceof V1_0_FederationClient) {
const pubKey = await client.getPublicKey()
if (pubKey && pubKey === dbCom.publicKey.toString()) { if (pubKey && pubKey === dbCom.publicKey.toString()) {
await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() }) await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() })
logger.info('Federation: verified community', dbCom) logger.info('Federation: verified community with', dbCom.endPoint)
} else { } else {
logger.warn( logger.warn(
'Federation: received not matching publicKey:', 'Federation: received not matching publicKey:',
@ -49,6 +53,7 @@ export async function validateCommunities(): Promise<void> {
dbCom.publicKey.toString(), dbCom.publicKey.toString(),
) )
} }
}
} catch (err) { } catch (err) {
logger.error(`Error:`, err) logger.error(`Error:`, err)
} }

View File

@ -322,8 +322,6 @@ export class TransactionResolver {
throw new LogError('Amount to send must be positive', amount) throw new LogError('Amount to send must be positive', amount)
} }
// TODO this is subject to replay attacks
// --- WHY?
const senderUser = getUser(context) const senderUser = getUser(context)
// validate recipient user // validate recipient user

View File

@ -0,0 +1,17 @@
import { Event as DbEvent } from '@entity/Event'
import { User } from '@entity/User'
import { UserContact } from '@entity/UserContact'
export const lastDateTimeEvents = async (
eventType: string,
): Promise<{ email: string; value: Date }[]> => {
return DbEvent.createQueryBuilder('event')
.select('MAX(event.created_at)', 'value')
.leftJoin(User, 'user', 'affected_user_id = user.id')
.leftJoin(UserContact, 'usercontact', 'user.id = usercontact.user_id')
.addSelect('usercontact.email', 'email')
.where('event.type = :eventType', { eventType })
.andWhere('usercontact.email IS NOT NULL')
.groupBy('event.affected_user_id')
.getRawMany()
}

View File

@ -1,14 +1,14 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/unbound-method */ /* eslint-disable @typescript-eslint/unbound-method */
import { Connection } from '@dbTools/typeorm' import { Connection as DbConnection } from '@dbTools/typeorm'
import { ApolloServer } from 'apollo-server-express' import { ApolloServer } from 'apollo-server-express'
import express, { Express, json, urlencoded } from 'express' import express, { Express, json, urlencoded } from 'express'
import { Logger } from 'log4js' import { Logger } from 'log4js'
import { CONFIG } from '@/config' import { CONFIG } from '@/config'
import { schema } from '@/graphql/schema' import { schema } from '@/graphql/schema'
import { connection } from '@/typeorm/connection' import { Connection } from '@/typeorm/connection'
import { checkDBVersion } from '@/typeorm/DBVersion' import { checkDBVersion } from '@/typeorm/DBVersion'
import { elopageWebhook } from '@/webhook/elopage' import { elopageWebhook } from '@/webhook/elopage'
@ -24,7 +24,7 @@ import { plugins } from './plugins'
interface ServerDef { interface ServerDef {
apollo: ApolloServer apollo: ApolloServer
app: Express app: Express
con: Connection con: DbConnection
} }
export const createServer = async ( export const createServer = async (
@ -37,7 +37,7 @@ export const createServer = async (
logger.debug('createServer...') logger.debug('createServer...')
// open mysql connection // open mysql connection
const con = await connection() const con = await Connection.getInstance()
if (!con?.isConnected) { if (!con?.isConnected) {
logger.fatal(`Couldn't open connection to database!`) logger.fatal(`Couldn't open connection to database!`)
throw new Error(`Fatal: Couldn't open connection to database`) throw new Error(`Fatal: Couldn't open connection to database`)

View File

@ -1,13 +1,33 @@
// TODO This is super weird - since the entities are defined in another project they have their own globals. // TODO This is super weird - since the entities are defined in another project they have their own globals.
// We cannot use our connection here, but must use the external typeorm installation // We cannot use our connection here, but must use the external typeorm installation
import { Connection, createConnection, FileLogger } from '@dbTools/typeorm' import { Connection as DbConnection, createConnection, FileLogger } from '@dbTools/typeorm'
import { entities } from '@entity/index' import { entities } from '@entity/index'
import { CONFIG } from '@/config' import { CONFIG } from '@/config'
export const connection = async (): Promise<Connection | null> => { // eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class Connection {
private static instance: DbConnection
/**
* The Singleton's constructor should always be private to prevent direct
* construction calls with the `new` operator.
*/
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
private constructor() {}
/**
* 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 async getInstance(): Promise<DbConnection | null> {
if (Connection.instance) {
return Connection.instance
}
try { try {
return createConnection({ Connection.instance = await createConnection({
name: 'default', name: 'default',
type: 'mysql', type: 'mysql',
host: CONFIG.DB_HOST, host: CONFIG.DB_HOST,
@ -25,9 +45,11 @@ export const connection = async (): Promise<Connection | null> => {
charset: 'utf8mb4_unicode_ci', charset: 'utf8mb4_unicode_ci',
}, },
}) })
return Connection.instance
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error) console.log(error)
return null return null
} }
} }
}

View File

@ -0,0 +1,16 @@
import { Connection } from '@/typeorm/connection'
import { exportEventDataToKlickTipp } from './klicktipp'
async function executeKlicktipp(): Promise<boolean> {
const connection = await Connection.getInstance()
if (connection) {
await exportEventDataToKlickTipp()
await connection.close()
return true
} else {
return false
}
}
void executeKlicktipp()

View File

@ -0,0 +1,65 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Connection } from '@dbTools/typeorm'
import { Event as DbEvent } from '@entity/Event'
import { ApolloServerTestClient } from 'apollo-server-testing'
import { testEnvironment, cleanDB, resetToken } from '@test/helpers'
import { addFieldsToSubscriber } from '@/apis/KlicktippController'
import { creations } from '@/seeds/creation'
import { creationFactory } from '@/seeds/factory/creation'
import { userFactory } from '@/seeds/factory/user'
import { login } from '@/seeds/graphql/mutations'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { peterLustig } from '@/seeds/users/peter-lustig'
import { exportEventDataToKlickTipp } from './klicktipp'
jest.mock('@/apis/KlicktippController')
let mutate: ApolloServerTestClient['mutate'], con: Connection
let testEnv: {
mutate: ApolloServerTestClient['mutate']
query: ApolloServerTestClient['query']
con: Connection
}
beforeAll(async () => {
testEnv = await testEnvironment()
mutate = testEnv.mutate
con = testEnv.con
await DbEvent.clear()
})
afterAll(async () => {
await cleanDB()
await con.close()
})
describe('klicktipp', () => {
beforeAll(async () => {
await userFactory(testEnv, bibiBloxberg)
await userFactory(testEnv, peterLustig)
const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de')
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await creationFactory(testEnv, bibisCreation!)
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(() => {
resetToken()
})
describe('exportEventDataToKlickTipp', () => {
it('calls the KlicktippController', async () => {
await exportEventDataToKlickTipp()
expect(addFieldsToSubscriber).toBeCalled()
})
})
})

View File

@ -1,14 +1,11 @@
// eslint-disable @typescript-eslint/no-explicit-any
import { User } from '@entity/User' import { User } from '@entity/User'
import { getKlickTippUser } from '@/apis/KlicktippController' import { getKlickTippUser, addFieldsToSubscriber } from '@/apis/KlicktippController'
import { LogError } from '@/server/LogError' import { EventType } from '@/event/EventType'
import { connection } from '@/typeorm/connection' import { lastDateTimeEvents } from '@/graphql/resolver/util/eventList'
export async function retrieveNotRegisteredEmails(): Promise<string[]> { export async function retrieveNotRegisteredEmails(): Promise<string[]> {
const con = await connection()
if (!con) {
throw new LogError('No connection to database')
}
const users = await User.find({ relations: ['emailContact'] }) const users = await User.find({ relations: ['emailContact'] })
const notRegisteredUser = [] const notRegisteredUser = []
for (const user of users) { for (const user of users) {
@ -20,10 +17,39 @@ export async function retrieveNotRegisteredEmails(): Promise<string[]> {
console.log(`${user.emailContact.email}`) console.log(`${user.emailContact.email}`)
} }
} }
await con.close()
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('User die nicht bei KlickTipp vorhanden sind: ', notRegisteredUser) console.log('User die nicht bei KlickTipp vorhanden sind: ', notRegisteredUser)
return notRegisteredUser return notRegisteredUser
} }
void retrieveNotRegisteredEmails() async function klickTippSendFieldToUser(
events: { email: string; value: Date }[],
field: string,
): Promise<void> {
for (const event of events) {
const time = event.value.setSeconds(0)
await addFieldsToSubscriber(event.email, { [field]: Math.trunc(time / 1000) })
}
}
export async function exportEventDataToKlickTipp(): Promise<boolean> {
const lastLoginEvents = await lastDateTimeEvents(EventType.USER_LOGIN)
await klickTippSendFieldToUser(lastLoginEvents, 'field186060')
const registeredEvents = await lastDateTimeEvents(EventType.USER_ACTIVATE_ACCOUNT)
await klickTippSendFieldToUser(registeredEvents, 'field186061')
const receiveTransactionEvents = await lastDateTimeEvents(EventType.TRANSACTION_RECEIVE)
await klickTippSendFieldToUser(receiveTransactionEvents, 'field185674')
const contributionCreateEvents = await lastDateTimeEvents(EventType.TRANSACTION_SEND)
await klickTippSendFieldToUser(contributionCreateEvents, 'field185673')
const linkRedeemedEvents = await lastDateTimeEvents(EventType.TRANSACTION_LINK_REDEEM)
await klickTippSendFieldToUser(linkRedeemedEvents, 'field185676')
const confirmContributionEvents = await lastDateTimeEvents(EventType.ADMIN_CONTRIBUTION_CONFIRM)
await klickTippSendFieldToUser(confirmContributionEvents, 'field185675')
return true
}

View File

@ -66,8 +66,6 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis
) )
} }
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) { export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn('ALTER TABLE `transactions` DROP COLUMN `user_gradido_id`;') await queryFn('ALTER TABLE `transactions` DROP COLUMN `user_gradido_id`;')
await queryFn('ALTER TABLE `transactions` DROP COLUMN `user_name`;') await queryFn('ALTER TABLE `transactions` DROP COLUMN `user_name`;')

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido-database", "name": "gradido-database",
"version": "1.20.0", "version": "1.21.0",
"description": "Gradido Database Tool to execute database migrations", "description": "Gradido Database Tool to execute database migrations",
"main": "src/index.ts", "main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/database", "repository": "https://github.com/gradido/gradido/database",

View File

@ -57,7 +57,7 @@ EMAIL_CODE_REQUEST_TIME=10
WEBHOOK_ELOPAGE_SECRET=secret WEBHOOK_ELOPAGE_SECRET=secret
# Federation # Federation
FEDERATION_DHT_CONFIG_VERSION=v2.2023-02-07 FEDERATION_DHT_CONFIG_VERSION=v3.2023-04-26
# 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=GRADIDO_HUB # FEDERATION_DHT_TOPIC=GRADIDO_HUB

View File

@ -8,6 +8,10 @@ DB_PASSWORD=$DB_PASSWORD
DB_DATABASE=gradido_community DB_DATABASE=gradido_community
TYPEORM_LOGGING_RELATIVE_PATH=$TYPEORM_LOGGING_RELATIVE_PATH TYPEORM_LOGGING_RELATIVE_PATH=$TYPEORM_LOGGING_RELATIVE_PATH
# Community
COMMUNITY_NAME=$COMMUNITY_NAME
COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION
# Federation # Federation
FEDERATION_DHT_CONFIG_VERSION=$FEDERATION_DHT_CONFIG_VERSION 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

View File

@ -6,7 +6,7 @@ module.exports = {
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
coverageThreshold: { coverageThreshold: {
global: { global: {
lines: 80, lines: 83,
}, },
}, },
setupFiles: ['<rootDir>/test/testSetup.ts'], setupFiles: ['<rootDir>/test/testSetup.ts'],

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido-dht-node", "name": "gradido-dht-node",
"version": "1.20.0", "version": "1.21.0",
"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/",
@ -23,7 +23,8 @@
"nodemon": "^2.0.20", "nodemon": "^2.0.20",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.2", "tsconfig-paths": "^4.1.2",
"typescript": "^4.9.4" "typescript": "^4.9.4",
"uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@types/dotenv": "^8.2.0", "@types/dotenv": "^8.2.0",
@ -31,6 +32,7 @@
"@types/node": "^18.11.18", "@types/node": "^18.11.18",
"@typescript-eslint/eslint-plugin": "^5.48.0", "@typescript-eslint/eslint-plugin": "^5.48.0",
"@typescript-eslint/parser": "^5.48.0", "@typescript-eslint/parser": "^5.48.0",
"@types/uuid": "^8.3.4",
"eslint": "^8.31.0", "eslint": "^8.31.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^17.0.0", "eslint-config-standard": "^17.0.0",

View File

@ -9,7 +9,7 @@ const constants = {
LOG_LEVEL: process.env.LOG_LEVEL || 'info', LOG_LEVEL: process.env.LOG_LEVEL || 'info',
CONFIG_VERSION: { CONFIG_VERSION: {
DEFAULT: 'DEFAULT', DEFAULT: 'DEFAULT',
EXPECTED: 'v2.2023-02-07', EXPECTED: 'v3.2023-04-26',
CURRENT: '', CURRENT: '',
}, },
} }
@ -28,6 +28,12 @@ const database = {
process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.dht-node.log', process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.dht-node.log',
} }
const community = {
COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung',
COMMUNITY_DESCRIPTION:
process.env.COMMUNITY_DESCRIPTION || 'Gradido-Community einer lokalen Entwicklungsumgebung.',
}
const federation = { const federation = {
FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || 'GRADIDO_HUB', FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || 'GRADIDO_HUB',
FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null, FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
@ -51,6 +57,7 @@ const CONFIG = {
...constants, ...constants,
...server, ...server,
...database, ...database,
...community,
...federation, ...federation,
} }

View File

@ -5,8 +5,10 @@ import { startDHT } from './index'
import DHT from '@hyperswarm/dht' import DHT from '@hyperswarm/dht'
import CONFIG from '@/config' import CONFIG from '@/config'
import { logger } from '@test/testSetup' import { logger } from '@test/testSetup'
import { Community as DbCommunity } from '@entity/Community'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { testEnvironment, cleanDB } from '@test/helpers' import { testEnvironment, cleanDB } from '@test/helpers'
import { validate as validateUUID, version as versionUUID } from 'uuid'
CONFIG.FEDERATION_DHT_SEED = '64ebcb0e3ad547848fef4197c6e2332f' CONFIG.FEDERATION_DHT_SEED = '64ebcb0e3ad547848fef4197c6e2332f'
@ -114,6 +116,9 @@ describe('federation', () => {
const hashSpy = jest.spyOn(DHT, 'hash') const hashSpy = jest.spyOn(DHT, 'hash')
const keyPairSpy = jest.spyOn(DHT, 'keyPair') const keyPairSpy = jest.spyOn(DHT, 'keyPair')
beforeEach(async () => { beforeEach(async () => {
CONFIG.FEDERATION_COMMUNITY_URL = 'https://test.gradido.net'
CONFIG.COMMUNITY_NAME = 'Gradido Test Community'
CONFIG.COMMUNITY_DESCRIPTION = 'Community to test the federation'
DHT.mockClear() DHT.mockClear()
jest.clearAllMocks() jest.clearAllMocks()
await cleanDB() await cleanDB()
@ -132,6 +137,64 @@ describe('federation', () => {
expect(DHT).toBeCalledWith({ keyPair: keyPairMock }) expect(DHT).toBeCalledWith({ keyPair: keyPairMock })
}) })
it('stores the home community in community table ', async () => {
const result = await DbCommunity.find()
expect(result).toEqual([
expect.objectContaining({
id: expect.any(Number),
foreign: false,
url: 'https://test.gradido.net/api/',
publicKey: expect.any(Buffer),
communityUuid: expect.any(String),
authenticatedAt: null,
name: 'Gradido Test Community',
description: 'Community to test the federation',
creationDate: expect.any(Date),
createdAt: expect.any(Date),
updatedAt: null,
}),
])
expect(validateUUID(result[0].communityUuid ? result[0].communityUuid : '')).toEqual(true)
expect(versionUUID(result[0].communityUuid ? result[0].communityUuid : '')).toEqual(4)
})
it('creates 3 entries in table federated_communities', async () => {
const result = await DbFederatedCommunity.find({ order: { id: 'ASC' } })
await expect(result).toHaveLength(3)
await expect(result).toEqual([
expect.objectContaining({
id: expect.any(Number),
foreign: false,
publicKey: expect.any(Buffer),
apiVersion: '1_0',
endPoint: 'https://test.gradido.net/api/',
lastAnnouncedAt: null,
createdAt: expect.any(Date),
updatedAt: null,
}),
expect.objectContaining({
id: expect.any(Number),
foreign: false,
publicKey: expect.any(Buffer),
apiVersion: '1_1',
endPoint: 'https://test.gradido.net/api/',
lastAnnouncedAt: null,
createdAt: expect.any(Date),
updatedAt: null,
}),
expect.objectContaining({
id: expect.any(Number),
foreign: false,
publicKey: expect.any(Buffer),
apiVersion: '2_0',
endPoint: 'https://test.gradido.net/api/',
lastAnnouncedAt: null,
createdAt: expect.any(Date),
updatedAt: null,
}),
])
})
describe('DHT node', () => { describe('DHT node', () => {
it('creates a server', () => { it('creates a server', () => {
expect(nodeCreateServerMock).toBeCalled() expect(nodeCreateServerMock).toBeCalled()
@ -780,21 +843,21 @@ describe('federation', () => {
socketEventMocks.open() socketEventMocks.open()
}) })
it.skip('calls socket write with own api versions', () => { it('calls socket write with own api versions', () => {
expect(socketWriteMock).toBeCalledWith( expect(socketWriteMock).toBeCalledWith(
Buffer.from( Buffer.from(
JSON.stringify([ JSON.stringify([
{ {
api: '1_0', api: '1_0',
url: 'http://localhost/api/', url: 'https://test.gradido.net/api/',
}, },
{ {
api: '1_1', api: '1_1',
url: 'http://localhost/api/', url: 'https://test.gradido.net/api/',
}, },
{ {
api: '2_0', api: '2_0',
url: 'http://localhost/api/', url: 'https://test.gradido.net/api/',
}, },
]), ]),
), ),
@ -804,5 +867,101 @@ describe('federation', () => {
}) })
}) })
}) })
describe('restart DHT', () => {
let homeCommunity: DbCommunity
let federatedCommunities: DbFederatedCommunity[]
describe('without changes', () => {
beforeEach(async () => {
DHT.mockClear()
jest.clearAllMocks()
homeCommunity = (await DbCommunity.find())[0]
federatedCommunities = await DbFederatedCommunity.find({ order: { id: 'ASC' } })
await startDHT(TEST_TOPIC)
})
it('does not change home community in community table except updated at column ', async () => {
await expect(DbCommunity.find()).resolves.toEqual([
{
...homeCommunity,
updatedAt: expect.any(Date),
},
])
})
it('rewrites the 3 entries in table federated_communities', async () => {
const result = await DbFederatedCommunity.find()
await expect(result).toHaveLength(3)
await expect(result).toEqual([
{
...federatedCommunities[0],
id: expect.any(Number),
createdAt: expect.any(Date),
},
{
...federatedCommunities[1],
id: expect.any(Number),
createdAt: expect.any(Date),
},
{
...federatedCommunities[2],
id: expect.any(Number),
createdAt: expect.any(Date),
},
])
})
})
describe('changeing URL, name and description', () => {
beforeEach(async () => {
CONFIG.FEDERATION_COMMUNITY_URL = 'https://test2.gradido.net'
CONFIG.COMMUNITY_NAME = 'Second Gradido Test Community'
CONFIG.COMMUNITY_DESCRIPTION = 'Another Community to test the federation'
DHT.mockClear()
jest.clearAllMocks()
homeCommunity = (await DbCommunity.find())[0]
federatedCommunities = await DbFederatedCommunity.find({ order: { id: 'ASC' } })
await startDHT(TEST_TOPIC)
})
it('updates URL, name, description and updated at columns ', async () => {
await expect(DbCommunity.find()).resolves.toEqual([
{
...homeCommunity,
url: 'https://test2.gradido.net/api/',
name: 'Second Gradido Test Community',
description: 'Another Community to test the federation',
updatedAt: expect.any(Date),
},
])
})
it('rewrites the 3 entries in table federated_communities with new endpoint', async () => {
const result = await DbFederatedCommunity.find()
await expect(result).toHaveLength(3)
await expect(result).toEqual([
{
...federatedCommunities[0],
id: expect.any(Number),
createdAt: expect.any(Date),
endPoint: 'https://test2.gradido.net/api/',
},
{
...federatedCommunities[1],
id: expect.any(Number),
createdAt: expect.any(Date),
endPoint: 'https://test2.gradido.net/api/',
},
{
...federatedCommunities[2],
id: expect.any(Number),
createdAt: expect.any(Date),
endPoint: 'https://test2.gradido.net/api/',
},
])
})
})
})
}) })
}) })

View File

@ -4,10 +4,15 @@ import DHT from '@hyperswarm/dht'
import { logger } from '@/server/logger' import { logger } from '@/server/logger'
import CONFIG from '@/config' import CONFIG from '@/config'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity' import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { Community as DbCommunity } from '@entity/Community'
import { v4 as uuidv4 } from 'uuid'
const KEY_SECRET_SEEDBYTES = 32 const KEY_SECRET_SEEDBYTES = 32
const getSeed = (): Buffer | null => const getSeed = (): Buffer | null => {
CONFIG.FEDERATION_DHT_SEED ? Buffer.alloc(KEY_SECRET_SEEDBYTES, CONFIG.FEDERATION_DHT_SEED) : null return CONFIG.FEDERATION_DHT_SEED
? Buffer.alloc(KEY_SECRET_SEEDBYTES, CONFIG.FEDERATION_DHT_SEED)
: null
}
const POLLTIME = 20000 const POLLTIME = 20000
const SUCCESSTIME = 120000 const SUCCESSTIME = 120000
@ -28,10 +33,12 @@ export const startDHT = async (topic: string): Promise<void> => {
try { try {
const TOPIC = DHT.hash(Buffer.from(topic)) const TOPIC = DHT.hash(Buffer.from(topic))
const keyPair = DHT.keyPair(getSeed()) const keyPair = DHT.keyPair(getSeed())
logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`) const pubKeyString = keyPair.publicKey.toString('hex')
logger.info(`keyPairDHT: publicKey=${pubKeyString}`)
logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`) logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`)
await writeHomeCommunityEntry(pubKeyString)
const ownApiVersions = await writeFederatedHomeCommunityEnries(keyPair.publicKey) const ownApiVersions = await writeFederatedHomeCommunityEntries(pubKeyString)
logger.info(`ApiList: ${JSON.stringify(ownApiVersions)}`) logger.info(`ApiList: ${JSON.stringify(ownApiVersions)}`)
const node = new DHT({ keyPair }) const node = new DHT({ keyPair })
@ -138,7 +145,7 @@ export const startDHT = async (topic: string): Promise<void> => {
data.peers.forEach((peer: any) => { data.peers.forEach((peer: any) => {
const pubKey = peer.publicKey.toString('hex') const pubKey = peer.publicKey.toString('hex')
if ( if (
pubKey !== keyPair.publicKey.toString('hex') && pubKey !== pubKeyString &&
!successfulRequests.includes(pubKey) && !successfulRequests.includes(pubKey) &&
!errorfulRequests.includes(pubKey) && !errorfulRequests.includes(pubKey) &&
!collectedPubKeys.includes(pubKey) !collectedPubKeys.includes(pubKey)
@ -179,7 +186,7 @@ export const startDHT = async (topic: string): Promise<void> => {
} }
} }
async function writeFederatedHomeCommunityEnries(pubKey: any): Promise<CommunityApi[]> { async function writeFederatedHomeCommunityEntries(pubKey: string): Promise<CommunityApi[]> {
const homeApiVersions: CommunityApi[] = Object.values(ApiVersionType).map(function (apiEnum) { const homeApiVersions: CommunityApi[] = Object.values(ApiVersionType).map(function (apiEnum) {
const comApi: CommunityApi = { const comApi: CommunityApi = {
api: apiEnum, api: apiEnum,
@ -189,21 +196,65 @@ async function writeFederatedHomeCommunityEnries(pubKey: any): Promise<Community
}) })
try { try {
// first remove privious existing homeCommunity entries // first remove privious existing homeCommunity entries
DbFederatedCommunity.createQueryBuilder().delete().where({ foreign: false }).execute() await DbFederatedCommunity.createQueryBuilder().delete().where({ foreign: false }).execute()
for (const homeApiVersion of homeApiVersions) {
homeApiVersions.forEach(async function (homeApi) { const homeCom = DbFederatedCommunity.create()
const homeCom = new DbFederatedCommunity()
homeCom.foreign = false homeCom.foreign = false
homeCom.apiVersion = homeApi.api homeCom.apiVersion = homeApiVersion.api
homeCom.endPoint = homeApi.url homeCom.endPoint = homeApiVersion.url
homeCom.publicKey = pubKey.toString('hex') homeCom.publicKey = Buffer.from(pubKey)
// this will NOT update the updatedAt column, to distingue between a normal update and the last announcement
await DbFederatedCommunity.insert(homeCom) await DbFederatedCommunity.insert(homeCom)
logger.info(`federation home-community inserted successfully: ${JSON.stringify(homeCom)}`) logger.info(`federation home-community inserted successfully:`, homeApiVersion)
}) }
} catch (err) { } catch (err) {
throw new Error(`Federation: Error writing HomeCommunity-Entries: ${err}`) throw new Error(`Federation: Error writing federated HomeCommunity-Entries: ${err}`)
} }
return homeApiVersions return homeApiVersions
} }
async function writeHomeCommunityEntry(pubKey: string): Promise<void> {
try {
// check for existing homeCommunity entry
let homeCom = await DbCommunity.findOne({
foreign: false,
publicKey: Buffer.from(pubKey),
})
if (!homeCom) {
// check if a homecommunity with a different publicKey still exists
homeCom = await DbCommunity.findOne({ foreign: false })
}
if (homeCom) {
// simply update the existing entry, but it MUST keep the ID and UUID because of possible relations
homeCom.publicKey = Buffer.from(pubKey)
homeCom.url = CONFIG.FEDERATION_COMMUNITY_URL + '/api/'
homeCom.name = CONFIG.COMMUNITY_NAME
homeCom.description = CONFIG.COMMUNITY_DESCRIPTION
await DbCommunity.save(homeCom)
logger.info(`home-community updated successfully:`, homeCom)
} else {
// insert a new homecommunity entry including a new ID and a new but ensured unique UUID
homeCom = new DbCommunity()
homeCom.foreign = false
homeCom.publicKey = Buffer.from(pubKey)
homeCom.communityUuid = await newCommunityUuid()
homeCom.url = CONFIG.FEDERATION_COMMUNITY_URL + '/api/'
homeCom.name = CONFIG.COMMUNITY_NAME
homeCom.description = CONFIG.COMMUNITY_DESCRIPTION
homeCom.creationDate = new Date()
await DbCommunity.insert(homeCom)
logger.info(`home-community inserted successfully:`, homeCom)
}
} catch (err) {
throw new Error(`Federation: Error writing HomeCommunity-Entry: ${err}`)
}
}
const newCommunityUuid = async (): Promise<string> => {
while (true) {
const communityUuid = uuidv4()
if ((await DbCommunity.count({ where: { communityUuid } })) === 0) {
return communityUuid
}
logger.info('CommunityUuid creation conflict...', communityUuid)
}
}

View File

@ -21,9 +21,8 @@ async function main() {
logger.fatal('Fatal: Database Version incorrect') logger.fatal('Fatal: Database Version incorrect')
throw new Error('Fatal: Database Version incorrect') throw new Error('Fatal: Database Version incorrect')
} }
logger.debug(`dhtseed set by CONFIG.FEDERATION_DHT_SEED=${CONFIG.FEDERATION_DHT_SEED}`)
// eslint-disable-next-line no-console logger.info(
console.log(
`starting Federation on ${CONFIG.FEDERATION_DHT_TOPIC} ${ `starting Federation on ${CONFIG.FEDERATION_DHT_TOPIC} ${
CONFIG.FEDERATION_DHT_SEED ? 'with seed...' : 'without seed...' CONFIG.FEDERATION_DHT_SEED ? 'with seed...' : 'without seed...'
}`, }`,

View File

@ -22,8 +22,8 @@ const context = {
export const cleanDB = async () => { export const cleanDB = async () => {
// this only works as long we do not have foreign key constraints // this only works as long we do not have foreign key constraints
for (let i = 0; i < entities.length; i++) { for (const entity of entities) {
await resetEntity(entities[i]) await resetEntity(entity)
} }
} }

View File

@ -769,6 +769,11 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
"@types/uuid@^8.3.4":
version "8.3.4"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
"@types/yargs-parser@*": "@types/yargs-parser@*":
version "21.0.0" version "21.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
@ -4138,6 +4143,11 @@ url-parse@^1.5.3:
querystringify "^2.1.1" querystringify "^2.1.1"
requires-port "^1.0.0" requires-port "^1.0.0"
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
v8-compile-cache-lib@^3.0.1: v8-compile-cache-lib@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido-federation", "name": "gradido-federation",
"version": "1.20.0", "version": "1.21.0",
"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

@ -1,6 +1,6 @@
{ {
"name": "bootstrap-vue-gradido-wallet", "name": "bootstrap-vue-gradido-wallet",
"version": "1.20.0", "version": "1.21.0",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "node run/server.js", "start": "node run/server.js",
@ -50,6 +50,7 @@
"prettier": "^2.2.1", "prettier": "^2.2.1",
"qrcanvas-vue": "2.1.1", "qrcanvas-vue": "2.1.1",
"regenerator-runtime": "^0.13.7", "regenerator-runtime": "^0.13.7",
"uuid": "^9.0.0",
"vee-validate": "^3.4.5", "vee-validate": "^3.4.5",
"vue": "2.6.12", "vue": "2.6.12",
"vue-apollo": "^3.0.7", "vue-apollo": "^3.0.7",

View File

@ -71,9 +71,9 @@ describe('TransactionForm', () => {
}) })
describe('with balance <= 0.00 GDD the form is disabled', () => { describe('with balance <= 0.00 GDD the form is disabled', () => {
it('has a disabled input field of type email', () => { it('has a disabled input field of type text', () => {
expect( expect(
wrapper.find('div[data-test="input-email"]').find('input').attributes('disabled'), wrapper.find('div[data-test="input-identifier"]').find('input').attributes('disabled'),
).toBe('disabled') ).toBe('disabled')
}) })
@ -116,51 +116,54 @@ describe('TransactionForm', () => {
expect(wrapper.vm.radioSelected).toBe(SEND_TYPES.send) expect(wrapper.vm.radioSelected).toBe(SEND_TYPES.send)
}) })
describe('email field', () => { describe('identifier field', () => {
it('has an input field of type email', () => { it('has an input field of type text', () => {
expect( expect(
wrapper.find('div[data-test="input-email"]').find('input').attributes('type'), wrapper.find('div[data-test="input-identifier"]').find('input').attributes('type'),
).toBe('email') ).toBe('text')
}) })
it('has a label form.receiver', () => { it('has a label form.recipient', () => {
expect(wrapper.find('div[data-test="input-email"]').find('label').text()).toBe( expect(wrapper.find('div[data-test="input-identifier"]').find('label').text()).toBe(
'form.recipient', 'form.recipient',
) )
}) })
it('has a placeholder "E-Mail"', () => { it('has a placeholder for identifier', () => {
expect( expect(
wrapper.find('div[data-test="input-email"]').find('input').attributes('placeholder'), wrapper
).toBe('form.email') .find('div[data-test="input-identifier"]')
.find('input')
.attributes('placeholder'),
).toBe('form.identifier')
}) })
it('flushes an error message when no valid email is given', async () => { it('flushes an error message when no valid identifier is given', async () => {
await wrapper.find('div[data-test="input-email"]').find('input').setValue('a') await wrapper.find('div[data-test="input-identifier"]').find('input').setValue('a')
await flushPromises() await flushPromises()
expect( expect(
wrapper.find('div[data-test="input-email"]').find('.invalid-feedback').text(), wrapper.find('div[data-test="input-identifier"]').find('.invalid-feedback').text(),
).toBe('validations.messages.email') ).toBe('form.validation.valid-identifier')
}) })
// TODO:SKIPPED there is no check that the email being sent to is the same as the user's email. // TODO:SKIPPED there is no check that the email being sent to is the same as the user's email.
it.skip('flushes an error message when email is the email of logged in user', async () => { it.skip('flushes an error message when email is the email of logged in user', async () => {
await wrapper await wrapper
.find('div[data-test="input-email"]') .find('div[data-test="input-identifier"]')
.find('input') .find('input')
.setValue('user@example.org') .setValue('user@example.org')
await flushPromises() await flushPromises()
expect( expect(
wrapper.find('div[data-test="input-email"]').find('.invalid-feedback').text(), wrapper.find('div[data-test="input-identifier"]').find('.invalid-feedback').text(),
).toBe('form.validation.is-not') ).toBe('form.validation.is-not')
}) })
it('trims the email after blur', async () => { it('trims the identifier after blur', async () => {
await wrapper await wrapper
.find('div[data-test="input-email"]') .find('div[data-test="input-identifier"]')
.find('input') .find('input')
.setValue(' valid@email.com ') .setValue(' valid@email.com ')
await wrapper.find('div[data-test="input-email"]').find('input').trigger('blur') await wrapper.find('div[data-test="input-identifier"]').find('input').trigger('blur')
await flushPromises() await flushPromises()
expect(wrapper.vm.form.identifier).toBe('valid@email.com') expect(wrapper.vm.form.identifier).toBe('valid@email.com')
}) })
@ -304,7 +307,7 @@ Die ganze Welt bezwingen.“`)
it('clears all fields on click', async () => { it('clears all fields on click', async () => {
await wrapper await wrapper
.find('div[data-test="input-email"]') .find('div[data-test="input-identifier"]')
.find('input') .find('input')
.setValue('someone@watches.tv') .setValue('someone@watches.tv')
await wrapper.find('div[data-test="input-amount"]').find('input').setValue('87.23') await wrapper.find('div[data-test="input-amount"]').find('input').setValue('87.23')
@ -327,7 +330,7 @@ Die ganze Welt bezwingen.“`)
describe('submit', () => { describe('submit', () => {
beforeEach(async () => { beforeEach(async () => {
await wrapper await wrapper
.find('div[data-test="input-email"]') .find('div[data-test="input-identifier"]')
.find('input') .find('input')
.setValue('someone@watches.tv') .setValue('someone@watches.tv')
await wrapper.find('div[data-test="input-amount"]').find('input').setValue('87.23') await wrapper.find('div[data-test="input-amount"]').find('input').setValue('87.23')
@ -380,8 +383,8 @@ Die ganze Welt bezwingen.“`)
}) })
describe('query for username with success', () => { describe('query for username with success', () => {
it('has no email input field', () => { it('has no identifier input field', () => {
expect(wrapper.find('div[data-test="input-email"]').exists()).toBe(false) expect(wrapper.find('div[data-test="input-identifier"]').exists()).toBe(false)
}) })
it('queries the username', () => { it('queries the username', () => {

View File

@ -59,10 +59,10 @@
</b-col> </b-col>
<b-col cols="12" v-if="radioSelected === sendTypes.send"> <b-col cols="12" v-if="radioSelected === sendTypes.send">
<div v-if="!gradidoID"> <div v-if="!gradidoID">
<input-email <input-identifier
:name="$t('form.recipient')" :name="$t('form.recipient')"
:label="$t('form.recipient')" :label="$t('form.recipient')"
:placeholder="$t('form.email')" :placeholder="$t('form.identifier')"
v-model="form.identifier" v-model="form.identifier"
:disabled="isBalanceDisabled" :disabled="isBalanceDisabled"
@onValidation="onValidation" @onValidation="onValidation"
@ -134,7 +134,7 @@
</template> </template>
<script> <script>
import { SEND_TYPES } from '@/pages/Send' import { SEND_TYPES } from '@/pages/Send'
import InputEmail from '@/components/Inputs/InputEmail' import InputIdentifier from '@/components/Inputs/InputIdentifier'
import InputAmount from '@/components/Inputs/InputAmount' import InputAmount from '@/components/Inputs/InputAmount'
import InputTextarea from '@/components/Inputs/InputTextarea' import InputTextarea from '@/components/Inputs/InputTextarea'
import { user as userQuery } from '@/graphql/queries' import { user as userQuery } from '@/graphql/queries'
@ -144,7 +144,7 @@ import { COMMUNITY_NAME } from '@/config'
export default { export default {
name: 'TransactionForm', name: 'TransactionForm',
components: { components: {
InputEmail, InputIdentifier,
InputAmount, InputAmount,
InputTextarea, InputTextarea,
}, },

View File

@ -0,0 +1,68 @@
<template>
<validation-provider
tag="div"
:rules="rules"
:name="name"
v-slot="{ errors, valid, validated, ariaInput, ariaMsg }"
>
<b-form-group :label="label" :label-for="labelFor" data-test="input-identifier">
<b-form-input
v-model="currentValue"
v-bind="ariaInput"
:id="labelFor"
:name="name"
:placeholder="placeholder"
type="text"
:state="validated ? valid : false"
trim
class="bg-248"
:disabled="disabled"
autocomplete="off"
></b-form-input>
<b-form-invalid-feedback v-bind="ariaMsg">
{{ errors[0] }}
</b-form-invalid-feedback>
</b-form-group>
</validation-provider>
</template>
<script>
export default {
name: 'InputEmail',
props: {
rules: {
default: () => {
return {
required: true,
validIdentifier: true,
}
},
},
name: { type: String, required: true },
label: { type: String, required: true },
placeholder: { type: String, required: true },
value: { type: String, required: true },
disabled: { type: Boolean, required: false, default: false },
},
data() {
return {
currentValue: this.value,
}
},
computed: {
labelFor() {
return this.name + '-input-field'
},
},
watch: {
currentValue() {
this.$emit('input', this.currentValue)
},
value() {
if (this.value !== this.currentValue) {
this.currentValue = this.value
}
this.$emit('onValidation')
},
},
}
</script>

View File

@ -29,10 +29,7 @@
</div> </div>
<div> <div>
<div data-test="navbar-item-username">{{ username.username }}</div> <div data-test="navbar-item-username">{{ username.username }}</div>
<div data-test="navbar-item-email">{{ $store.state.email }}</div>
<div data-test="navbar-item-email">
{{ $store.state.email }}
</div>
</div> </div>
</div> </div>
</router-link> </router-link>

View File

@ -142,6 +142,7 @@
"from": "Von", "from": "Von",
"generate_now": "Jetzt generieren", "generate_now": "Jetzt generieren",
"hours": "Stunden", "hours": "Stunden",
"identifier": "Email, Nutzername oder Gradido ID",
"lastname": "Nachname", "lastname": "Nachname",
"memo": "Nachricht", "memo": "Nachricht",
"message": "Nachricht", "message": "Nachricht",
@ -175,7 +176,8 @@
"is-not": "Du kannst dir selbst keine Gradidos überweisen", "is-not": "Du kannst dir selbst keine Gradidos überweisen",
"username-allowed-chars": "Der Nutzername darf nur aus Buchstaben (ohne Umlaute), Zahlen, Binde- oder Unterstrichen bestehen.", "username-allowed-chars": "Der Nutzername darf nur aus Buchstaben (ohne Umlaute), Zahlen, Binde- oder Unterstrichen bestehen.",
"username-hyphens": "Binde- oder Unterstriche müssen zwischen Buchstaben oder Zahlen stehen.", "username-hyphens": "Binde- oder Unterstriche müssen zwischen Buchstaben oder Zahlen stehen.",
"username-unique": "Der Nutzername ist bereits vergeben." "username-unique": "Der Nutzername ist bereits vergeben.",
"valid-identifier": "Muss eine Email, ein Nutzernamen oder eine gradido ID sein."
}, },
"your_amount": "Dein Betrag" "your_amount": "Dein Betrag"
}, },

View File

@ -142,6 +142,7 @@
"from": "from", "from": "from",
"generate_now": "Generate now", "generate_now": "Generate now",
"hours": "Hours", "hours": "Hours",
"identifier": "Email, username or gradido ID",
"lastname": "Lastname", "lastname": "Lastname",
"memo": "Message", "memo": "Message",
"message": "Message", "message": "Message",
@ -175,7 +176,8 @@
"is-not": "You cannot send Gradidos to yourself", "is-not": "You cannot send Gradidos to yourself",
"username-allowed-chars": "The username may only contain letters, numbers, hyphens or underscores.", "username-allowed-chars": "The username may only contain letters, numbers, hyphens or underscores.",
"username-hyphens": "Hyphens or underscores must be in between letters or numbers.", "username-hyphens": "Hyphens or underscores must be in between letters or numbers.",
"username-unique": "This username is already taken." "username-unique": "This username is already taken.",
"valid-identifier": "Must be a valid email, username or gradido ID."
}, },
"your_amount": "Your amount" "your_amount": "Your amount"
}, },

View File

@ -146,6 +146,10 @@ describe('Login', () => {
expect(mockStoreDispach).toBeCalledWith('login', 'token') expect(mockStoreDispach).toBeCalledWith('login', 'token')
}) })
it('commits email to store', () => {
expect(mockStoreCommit).toBeCalledWith('email', 'user@example.org')
})
it('hides the spinner', () => { it('hides the spinner', () => {
expect(spinnerHideMock).toBeCalled() expect(spinnerHideMock).toBeCalled()
}) })

View File

@ -100,6 +100,7 @@ export default {
data: { login }, data: { login },
} = result } = result
this.$store.dispatch('login', login) this.$store.dispatch('login', login)
this.$store.commit('email', this.form.email)
await loader.hide() await loader.hide()
if (this.$route.params.code) { if (this.$route.params.code) {
this.$router.push(`/redeem/${this.$route.params.code}`) this.$router.push(`/redeem/${this.$route.params.code}`)

View File

@ -66,8 +66,11 @@ describe('Send', () => {
beforeEach(async () => { beforeEach(async () => {
const transactionForm = wrapper.findComponent({ name: 'TransactionForm' }) const transactionForm = wrapper.findComponent({ name: 'TransactionForm' })
await transactionForm.findAll('input[type="radio"]').at(0).setChecked() await transactionForm.findAll('input[type="radio"]').at(0).setChecked()
await transactionForm.find('input[type="email"]').setValue('user@example.org') await transactionForm
await transactionForm.find('input[type="text"]').setValue('23.45') .find('[data-test="input-identifier"]')
.find('input')
.setValue('user@example.org')
await transactionForm.find('[data-test="input-amount"]').find('input').setValue('23.45')
await transactionForm.find('textarea').setValue('Make the best of it!') await transactionForm.find('textarea').setValue('Make the best of it!')
await transactionForm.find('form').trigger('submit') await transactionForm.find('form').trigger('submit')
await flushPromises() await flushPromises()
@ -91,8 +94,12 @@ describe('Send', () => {
}) })
it('restores the previous data in the formular', () => { it('restores the previous data in the formular', () => {
expect(wrapper.find("input[type='email']").vm.$el.value).toBe('user@example.org') expect(wrapper.find('[data-test="input-identifier"]').find('input').vm.$el.value).toBe(
expect(wrapper.find("input[type='text']").vm.$el.value).toBe('23.45') 'user@example.org',
)
expect(wrapper.find('[data-test="input-amount"]').find('input').vm.$el.value).toBe(
'23.45',
)
expect(wrapper.find('textarea').vm.$el.value).toBe('Make the best of it!') expect(wrapper.find('textarea').vm.$el.value).toBe('Make the best of it!')
}) })
}) })
@ -175,7 +182,10 @@ describe('Send', () => {
it('has no email input field', () => { it('has no email input field', () => {
expect( expect(
wrapper.findComponent({ name: 'TransactionForm' }).find('input[type="email"]').exists(), wrapper
.findComponent({ name: 'TransactionForm' })
.find('[data-test="input-identifier"]')
.exists(),
).toBe(false) ).toBe(false)
}) })
@ -183,7 +193,7 @@ describe('Send', () => {
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks() jest.clearAllMocks()
const transactionForm = wrapper.findComponent({ name: 'TransactionForm' }) const transactionForm = wrapper.findComponent({ name: 'TransactionForm' })
await transactionForm.find('input[type="text"]').setValue('34.56') await transactionForm.find('[data-test="input-amount"]').find('input').setValue('34.56')
await transactionForm.find('textarea').setValue('Make the best of it!') await transactionForm.find('textarea').setValue('Make the best of it!')
await transactionForm.find('form').trigger('submit') await transactionForm.find('form').trigger('submit')
await flushPromises() await flushPromises()
@ -243,7 +253,7 @@ describe('Send', () => {
}) })
const transactionForm = wrapper.findComponent({ name: 'TransactionForm' }) const transactionForm = wrapper.findComponent({ name: 'TransactionForm' })
await transactionForm.findAll('input[type="radio"]').at(1).setChecked() await transactionForm.findAll('input[type="radio"]').at(1).setChecked()
await transactionForm.find('input[type="text"]').setValue('56.78') await transactionForm.find('[data-test="input-amount"]').find('input').setValue('56.78')
await transactionForm.find('textarea').setValue('Make the best of the link!') await transactionForm.find('textarea').setValue('Make the best of the link!')
await transactionForm.find('form').trigger('submit') await transactionForm.find('form').trigger('submit')
await flushPromises() await flushPromises()

View File

@ -53,6 +53,9 @@ export const mutations = {
hideAmountGDT: (state, hideAmountGDT) => { hideAmountGDT: (state, hideAmountGDT) => {
state.hideAmountGDT = !!hideAmountGDT state.hideAmountGDT = !!hideAmountGDT
}, },
email: (state, email) => {
state.email = email || ''
},
} }
export const actions = { export const actions = {
@ -81,6 +84,7 @@ export const actions = {
commit('isAdmin', false) commit('isAdmin', false)
commit('hideAmountGDD', false) commit('hideAmountGDD', false)
commit('hideAmountGDT', true) commit('hideAmountGDT', true)
commit('email', '')
localStorage.clear() localStorage.clear()
}, },
} }
@ -109,6 +113,7 @@ try {
publisherId: null, publisherId: null,
hideAmountGDD: null, hideAmountGDD: null,
hideAmountGDT: null, hideAmountGDT: null,
email: '',
}, },
getters: {}, getters: {},
// Syncronous mutation of the state // Syncronous mutation of the state

View File

@ -33,6 +33,7 @@ const {
hasElopage, hasElopage,
hideAmountGDD, hideAmountGDD,
hideAmountGDT, hideAmountGDT,
email,
} = mutations } = mutations
const { login, logout } = actions const { login, logout } = actions
@ -166,6 +167,14 @@ describe('Vuex store', () => {
expect(state.hideAmountGDT).toEqual(true) expect(state.hideAmountGDT).toEqual(true)
}) })
}) })
describe('email', () => {
it('sets the state of email', () => {
const state = { email: '' }
email(state, 'peter@luatig.de')
expect(state.email).toEqual('peter@luatig.de')
})
})
}) })
describe('actions', () => { describe('actions', () => {
@ -253,9 +262,9 @@ describe('Vuex store', () => {
const commit = jest.fn() const commit = jest.fn()
const state = {} const state = {}
it('calls eleven commits', () => { it('calls twelve commits', () => {
logout({ commit, state }) logout({ commit, state })
expect(commit).toHaveBeenCalledTimes(11) expect(commit).toHaveBeenCalledTimes(12)
}) })
it('commits token', () => { it('commits token', () => {
@ -312,6 +321,12 @@ describe('Vuex store', () => {
logout({ commit, state }) logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(11, 'hideAmountGDT', true) expect(commit).toHaveBeenNthCalledWith(11, 'hideAmountGDT', true)
}) })
it('commits email', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(12, 'email', '')
})
// how to get this working? // how to get this working?
it.skip('calls localStorage.clear()', () => { it.skip('calls localStorage.clear()', () => {
const clearStorageMock = jest.fn() const clearStorageMock = jest.fn()

View File

@ -2,6 +2,13 @@ import { configure, extend } from 'vee-validate'
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
import { required, email, min, max, is_not } from 'vee-validate/dist/rules' import { required, email, min, max, is_not } from 'vee-validate/dist/rules'
import { checkUsername } from '@/graphql/queries' import { checkUsername } from '@/graphql/queries'
import { validate as validateUuid, version as versionUuid } from 'uuid'
// taken from vee-validate
// eslint-disable-next-line no-useless-escape
const EMAIL_REGEX = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
const USERNAME_REGEX = /^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/
export const loadAllRules = (i18nCallback, apollo) => { export const loadAllRules = (i18nCallback, apollo) => {
configure({ configure({
@ -141,7 +148,7 @@ export const loadAllRules = (i18nCallback, apollo) => {
extend('usernameUnique', { extend('usernameUnique', {
validate(value) { validate(value) {
if (!value.match(/^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/)) return true if (!value.match(USERNAME_REGEX)) return true
return apollo return apollo
.query({ .query({
query: checkUsername, query: checkUsername,
@ -155,4 +162,14 @@ export const loadAllRules = (i18nCallback, apollo) => {
}, },
message: (_, values) => i18nCallback.t('form.validation.username-unique', values), message: (_, values) => i18nCallback.t('form.validation.username-unique', values),
}) })
extend('validIdentifier', {
validate(value) {
const isEmail = !!EMAIL_REGEX.test(value)
const isUsername = !!value.match(USERNAME_REGEX)
const isGradidoId = validateUuid(value) && versionUuid(value) === 4
return isEmail || isUsername || isGradidoId
},
message: (_, values) => i18nCallback.t('form.validation.valid-identifier', values),
})
} }

View File

@ -14176,6 +14176,11 @@ uuid@^8.3.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0: v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"

View File

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