Merge remote-tracking branch 'origin/2946-feature-x-com-3-introduce-business-communities' into 2956-feature-x-com-4-introduce-public-community-info-handshake

This commit is contained in:
Claus-Peter Huebner 2023-05-22 23:50:07 +02:00
commit 2cf2f8fcd4
32 changed files with 461 additions and 103 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).
#### [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)
> 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(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)

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "gradido-backend",
"version": "1.20.0",
"version": "1.21.0",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend",
@ -15,7 +15,7 @@
"lint": "eslint --max-warnings=0 .",
"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",
"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"
},
"dependencies": {

View File

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

View File

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

View File

@ -8,6 +8,8 @@
import { Connection } from '@dbTools/typeorm'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
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 { logger } from '@test/testSetup'
@ -59,6 +61,17 @@ describe('validate Communities', () => {
describe('with one Community of api 1_0', () => {
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 = {
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '1_0',
@ -88,9 +101,28 @@ describe('validate Communities', () => {
'http//localhost:5001/api/1_0/',
)
})
it('logs not matching publicKeys', () => {
expect(logger.warn).toBeCalledWith(
'Federation: received not matching publicKey:',
'somePubKey',
expect.stringMatching('11111111111111111111111111111111'),
)
})
})
describe('with two Communities of api 1_0 and 1_1', () => {
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: '11111111111111111111111111111111',
},
},
} as Response<unknown>
})
const variables2 = {
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '1_1',

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/restrict-template-expressions */
/* 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 express, { Express, json, urlencoded } from 'express'
import { Logger } from 'log4js'
import { CONFIG } from '@/config'
import { schema } from '@/graphql/schema'
import { connection } from '@/typeorm/connection'
import { Connection } from '@/typeorm/connection'
import { checkDBVersion } from '@/typeorm/DBVersion'
import { elopageWebhook } from '@/webhook/elopage'
@ -24,7 +24,7 @@ import { plugins } from './plugins'
interface ServerDef {
apollo: ApolloServer
app: Express
con: Connection
con: DbConnection
}
export const createServer = async (
@ -37,7 +37,7 @@ export const createServer = async (
logger.debug('createServer...')
// open mysql connection
const con = await connection()
const con = await Connection.getInstance()
if (!con?.isConnected) {
logger.fatal(`Couldn't open connection to database!`)
throw new Error(`Fatal: Couldn't open connection to database`)

View File

@ -1,33 +1,55 @@
// 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
import { Connection, createConnection, FileLogger } from '@dbTools/typeorm'
import { Connection as DbConnection, createConnection, FileLogger } from '@dbTools/typeorm'
import { entities } from '@entity/index'
import { CONFIG } from '@/config'
export const connection = async (): Promise<Connection | null> => {
try {
return createConnection({
name: 'default',
type: 'mysql',
host: CONFIG.DB_HOST,
port: CONFIG.DB_PORT,
username: CONFIG.DB_USER,
password: CONFIG.DB_PASSWORD,
database: CONFIG.DB_DATABASE,
entities,
synchronize: false,
logging: true,
logger: new FileLogger('all', {
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
}),
extra: {
charset: 'utf8mb4_unicode_ci',
},
})
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)
return 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 {
Connection.instance = await createConnection({
name: 'default',
type: 'mysql',
host: CONFIG.DB_HOST,
port: CONFIG.DB_PORT,
username: CONFIG.DB_USER,
password: CONFIG.DB_PASSWORD,
database: CONFIG.DB_DATABASE,
entities,
synchronize: false,
logging: true,
logger: new FileLogger('all', {
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
}),
extra: {
charset: 'utf8mb4_unicode_ci',
},
})
return Connection.instance
} catch (error) {
// eslint-disable-next-line no-console
console.log(error)
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 { getKlickTippUser } from '@/apis/KlicktippController'
import { LogError } from '@/server/LogError'
import { connection } from '@/typeorm/connection'
import { getKlickTippUser, addFieldsToSubscriber } from '@/apis/KlicktippController'
import { EventType } from '@/event/EventType'
import { lastDateTimeEvents } from '@/graphql/resolver/util/eventList'
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 notRegisteredUser = []
for (const user of users) {
@ -20,10 +17,39 @@ export async function retrieveNotRegisteredEmails(): Promise<string[]> {
console.log(`${user.emailContact.email}`)
}
}
await con.close()
// eslint-disable-next-line no-console
console.log('User die nicht bei KlickTipp vorhanden sind: ', 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

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

View File

@ -57,7 +57,7 @@ EMAIL_CODE_REQUEST_TIME=10
WEBHOOK_ELOPAGE_SECRET=secret
# 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
# on an hash created from this topic
# FEDERATION_DHT_TOPIC=GRADIDO_HUB

View File

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

View File

@ -195,11 +195,7 @@ describe('federation', () => {
const resultBefore = await DbCommunity.find({ foreign: false })
expect(resultBefore).toHaveLength(1)
const modifiedCom = DbCommunity.create()
modifiedCom.communityUuid = resultBefore[0].communityUuid
modifiedCom.creationDate = resultBefore[0].creationDate
modifiedCom.description = 'updated description'
modifiedCom.foreign = resultBefore[0].foreign
modifiedCom.id = resultBefore[0].id
modifiedCom.name = 'update name'
modifiedCom.publicKey = Buffer.from(
'1234567891abcdef7892abcdef7893abcdef7894abcdef7895abcdef7896abcd',

View File

@ -1,6 +1,6 @@
{
"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",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/federation",

View File

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

View File

@ -71,9 +71,9 @@ describe('TransactionForm', () => {
})
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(
wrapper.find('div[data-test="input-email"]').find('input').attributes('disabled'),
wrapper.find('div[data-test="input-identifier"]').find('input').attributes('disabled'),
).toBe('disabled')
})
@ -116,51 +116,54 @@ describe('TransactionForm', () => {
expect(wrapper.vm.radioSelected).toBe(SEND_TYPES.send)
})
describe('email field', () => {
it('has an input field of type email', () => {
describe('identifier field', () => {
it('has an input field of type text', () => {
expect(
wrapper.find('div[data-test="input-email"]').find('input').attributes('type'),
).toBe('email')
wrapper.find('div[data-test="input-identifier"]').find('input').attributes('type'),
).toBe('text')
})
it('has a label form.receiver', () => {
expect(wrapper.find('div[data-test="input-email"]').find('label').text()).toBe(
it('has a label form.recipient', () => {
expect(wrapper.find('div[data-test="input-identifier"]').find('label').text()).toBe(
'form.recipient',
)
})
it('has a placeholder "E-Mail"', () => {
it('has a placeholder for identifier', () => {
expect(
wrapper.find('div[data-test="input-email"]').find('input').attributes('placeholder'),
).toBe('form.email')
wrapper
.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 () => {
await wrapper.find('div[data-test="input-email"]').find('input').setValue('a')
it('flushes an error message when no valid identifier is given', async () => {
await wrapper.find('div[data-test="input-identifier"]').find('input').setValue('a')
await flushPromises()
expect(
wrapper.find('div[data-test="input-email"]').find('.invalid-feedback').text(),
).toBe('validations.messages.email')
wrapper.find('div[data-test="input-identifier"]').find('.invalid-feedback').text(),
).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.
it.skip('flushes an error message when email is the email of logged in user', async () => {
await wrapper
.find('div[data-test="input-email"]')
.find('div[data-test="input-identifier"]')
.find('input')
.setValue('user@example.org')
await flushPromises()
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')
})
it('trims the email after blur', async () => {
it('trims the identifier after blur', async () => {
await wrapper
.find('div[data-test="input-email"]')
.find('div[data-test="input-identifier"]')
.find('input')
.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()
expect(wrapper.vm.form.identifier).toBe('valid@email.com')
})
@ -304,7 +307,7 @@ Die ganze Welt bezwingen.“`)
it('clears all fields on click', async () => {
await wrapper
.find('div[data-test="input-email"]')
.find('div[data-test="input-identifier"]')
.find('input')
.setValue('someone@watches.tv')
await wrapper.find('div[data-test="input-amount"]').find('input').setValue('87.23')
@ -327,7 +330,7 @@ Die ganze Welt bezwingen.“`)
describe('submit', () => {
beforeEach(async () => {
await wrapper
.find('div[data-test="input-email"]')
.find('div[data-test="input-identifier"]')
.find('input')
.setValue('someone@watches.tv')
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', () => {
it('has no email input field', () => {
expect(wrapper.find('div[data-test="input-email"]').exists()).toBe(false)
it('has no identifier input field', () => {
expect(wrapper.find('div[data-test="input-identifier"]').exists()).toBe(false)
})
it('queries the username', () => {

View File

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

View File

@ -142,6 +142,7 @@
"from": "Von",
"generate_now": "Jetzt generieren",
"hours": "Stunden",
"identifier": "Email, Nutzername oder Gradido ID",
"lastname": "Nachname",
"memo": "Nachricht",
"message": "Nachricht",
@ -175,7 +176,8 @@
"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-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"
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,13 @@ import { configure, extend } from 'vee-validate'
// eslint-disable-next-line camelcase
import { required, email, min, max, is_not } from 'vee-validate/dist/rules'
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) => {
configure({
@ -141,7 +148,7 @@ export const loadAllRules = (i18nCallback, apollo) => {
extend('usernameUnique', {
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
.query({
query: checkUsername,
@ -155,4 +162,14 @@ export const loadAllRules = (i18nCallback, apollo) => {
},
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"
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:
version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"

View File

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