mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge remote-tracking branch 'origin/master' into 2946-feature-x-com-3-introduce-business-communities
This commit is contained in:
commit
529a7018ba
@ -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": {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
17
backend/src/graphql/resolver/util/eventList.ts
Normal file
17
backend/src/graphql/resolver/util/eventList.ts
Normal 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()
|
||||
}
|
||||
@ -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`)
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
16
backend/src/util/executeKlicktipp.ts
Normal file
16
backend/src/util/executeKlicktipp.ts
Normal 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()
|
||||
65
backend/src/util/klicktipp.test.ts
Normal file
65
backend/src/util/klicktipp.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
68
frontend/src/components/Inputs/InputIdentifier.vue
Normal file
68
frontend/src/components/Inputs/InputIdentifier.vue
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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()
|
||||
})
|
||||
|
||||
@ -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}`)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user