Merge branch 'docu-env-vars' of github.com:gradido/gradido into docu-env-vars

This commit is contained in:
Moriz Wahl 2022-07-19 00:23:04 +02:00
commit f2e34938c5
26 changed files with 1048 additions and 326 deletions

View File

@ -1,4 +1,4 @@
CONFIG_VERSION=v8.2022-06-20
CONFIG_VERSION=v9.2022-07-07
# Server
PORT=4000
@ -52,6 +52,9 @@ EMAIL_CODE_REQUEST_TIME=10
# Webhook
WEBHOOK_ELOPAGE_SECRET=secret
# EventProtocol
EVENT_PROTOCOL_DISABLED=false
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
# LOG_LEVEL=info

View File

@ -50,3 +50,6 @@ EMAIL_CODE_REQUEST_TIME=$EMAIL_CODE_REQUEST_TIME
# Webhook
WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET
# EventProtocol
EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED

View File

@ -1,16 +1,18 @@
# backend
## Project setup
```
```bash
yarn install
```
## Seed DB
```
```bash
yarn seed
```
Deletes all data in database. Then seeds data in database.
Deletes all data in database. Then seeds data in database.
## Seeded Users
@ -22,3 +24,47 @@ Deletes all data in database. Then seeds data in database.
| bob@baumeister.de | `Aa12345_` | `false` | `true` | `false` |
| garrick@ollivander.com | | `false` | `false` | `false` |
| stephen@hawking.uk | `Aa12345_` | `false` | `true` | `true` |
## Setup GraphQL Playground
### Setup In The Code
Setting up the GraphQL Playground in our code requires the following steps:
- Create an empty `.env` file in the `backend` folder and set "GRAPHIQL=true" there.
- Start or restart Docker Compose.
- For verification, Docker should display `GraphQL available at http://localhost:4000` in the terminal.
- If you open "http://localhost:4000/" in your browser, you should see the GraphQL Playground.
### Authentication
You need to authenticate yourself in GraphQL Playground to be able to send queries and mutations, to do so follow the steps below:
- in Firefox go to "Network Analysis" and delete all entries
- enter and send the login query:
```gql
{
login(email: "bibi@bloxberg.de", password:"Aa12345_") {
id
publisherId
email
firstName
lastName
emailChecked
language
hasElopage
}
}
```
- search in Firefox under „Network Analysis" for the smallest size of a header and copy the value of the token
- open the header section in GraphQL Playground and set your current token by filling in and replacing `XXX`:
```qgl
{
"Authorization": "XXX"
}
```
Now you can open a new tap in the Playground and enter your query or mutation there.

View File

@ -27,6 +27,7 @@ export enum RIGHTS {
GDT_BALANCE = 'GDT_BALANCE',
CREATE_CONTRIBUTION = 'CREATE_CONTRIBUTION',
LIST_CONTRIBUTIONS = 'LIST_CONTRIBUTIONS',
LIST_ALL_CONTRIBUTIONS = 'LIST_ALL_CONTRIBUTIONS',
UPDATE_CONTRIBUTION = 'UPDATE_CONTRIBUTION',
// Admin
SEARCH_USERS = 'SEARCH_USERS',

View File

@ -25,6 +25,7 @@ export const ROLE_USER = new Role('user', [
RIGHTS.GDT_BALANCE,
RIGHTS.CREATE_CONTRIBUTION,
RIGHTS.LIST_CONTRIBUTIONS,
RIGHTS.LIST_ALL_CONTRIBUTIONS,
RIGHTS.UPDATE_CONTRIBUTION,
])
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights

View File

@ -10,14 +10,14 @@ Decimal.set({
})
const constants = {
DB_VERSION: '0042-update_transactions_for_blockchain',
DB_VERSION: '0043-add_event_protocol_table',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v8.2022-06-20',
EXPECTED: 'v9.2022-07-07',
CURRENT: '',
},
}
@ -94,6 +94,11 @@ const webhook = {
WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret',
}
const eventProtocol = {
// global switch to enable writing of EventProtocol-Entries
EVENT_PROTOCOL_DISABLED: process.env.EVENT_PROTOCOL_DISABLED === 'true' || false,
}
// This is needed by graphql-directive-auth
process.env.APP_SECRET = server.JWT_SECRET
@ -118,6 +123,7 @@ const CONFIG = {
...email,
...loginServer,
...webhook,
...eventProtocol,
}
export default CONFIG

301
backend/src/event/Event.ts Normal file
View File

@ -0,0 +1,301 @@
import { EventProtocol } from '@entity/EventProtocol'
import decimal from 'decimal.js-light'
import { EventProtocolType } from './EventProtocolType'
export class EventBasic {
type: string
createdAt: Date
}
export class EventBasicUserId extends EventBasic {
userId: number
}
export class EventBasicTx extends EventBasicUserId {
xUserId: number
xCommunityId: number
transactionId: number
amount: decimal
}
export class EventBasicCt extends EventBasicUserId {
contributionId: number
amount: decimal
}
export class EventBasicRedeem extends EventBasicUserId {
transactionId?: number
contributionId?: number
}
export class EventVisitGradido extends EventBasic {}
export class EventRegister extends EventBasicUserId {}
export class EventRedeemRegister extends EventBasicRedeem {}
export class EventInactiveAccount extends EventBasicUserId {}
export class EventSendConfirmationEmail extends EventBasicUserId {}
export class EventConfirmationEmail extends EventBasicUserId {}
export class EventRegisterEmailKlicktipp extends EventBasicUserId {}
export class EventLogin extends EventBasicUserId {}
export class EventRedeemLogin extends EventBasicRedeem {}
export class EventActivateAccount extends EventBasicUserId {}
export class EventPasswordChange extends EventBasicUserId {}
export class EventTransactionSend extends EventBasicTx {}
export class EventTransactionSendRedeem extends EventBasicTx {}
export class EventTransactionRepeateRedeem extends EventBasicTx {}
export class EventTransactionCreation extends EventBasicUserId {
transactionId: number
amount: decimal
}
export class EventTransactionReceive extends EventBasicTx {}
export class EventTransactionReceiveRedeem extends EventBasicTx {}
export class EventContributionCreate extends EventBasicCt {}
export class EventContributionConfirm extends EventBasicCt {
xUserId: number
xCommunityId: number
}
export class EventContributionLinkDefine extends EventBasicCt {}
export class EventContributionLinkActivateRedeem extends EventBasicCt {}
export class Event {
constructor()
constructor(event?: EventProtocol) {
if (event) {
this.id = event.id
this.type = event.type
this.createdAt = event.createdAt
this.userId = event.userId
this.xUserId = event.xUserId
this.xCommunityId = event.xCommunityId
this.transactionId = event.transactionId
this.contributionId = event.contributionId
this.amount = event.amount
}
}
public setEventBasic(): Event {
this.type = EventProtocolType.BASIC
this.createdAt = new Date()
return this
}
public setEventVisitGradido(): Event {
this.setEventBasic()
this.type = EventProtocolType.VISIT_GRADIDO
return this
}
public setEventRegister(ev: EventRegister): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.REGISTER
return this
}
public setEventRedeemRegister(ev: EventRedeemRegister): Event {
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId)
this.type = EventProtocolType.REDEEM_REGISTER
return this
}
public setEventInactiveAccount(ev: EventInactiveAccount): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.INACTIVE_ACCOUNT
return this
}
public setEventSendConfirmationEmail(ev: EventSendConfirmationEmail): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.SEND_CONFIRMATION_EMAIL
return this
}
public setEventConfirmationEmail(ev: EventConfirmationEmail): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.CONFIRM_EMAIL
return this
}
public setEventRegisterEmailKlicktipp(ev: EventRegisterEmailKlicktipp): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.REGISTER_EMAIL_KLICKTIPP
return this
}
public setEventLogin(ev: EventLogin): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.LOGIN
return this
}
public setEventRedeemLogin(ev: EventRedeemLogin): Event {
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId)
this.type = EventProtocolType.REDEEM_LOGIN
return this
}
public setEventActivateAccount(ev: EventActivateAccount): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.ACTIVATE_ACCOUNT
return this
}
public setEventPasswordChange(ev: EventPasswordChange): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.PASSWORD_CHANGE
return this
}
public setEventTransactionSend(ev: EventTransactionSend): Event {
this.setByBasicTx(ev.userId, ev.xUserId, ev.xCommunityId, ev.transactionId, ev.amount)
this.type = EventProtocolType.TRANSACTION_SEND
return this
}
public setEventTransactionSendRedeem(ev: EventTransactionSendRedeem): Event {
this.setByBasicTx(ev.userId, ev.xUserId, ev.xCommunityId, ev.transactionId, ev.amount)
this.type = EventProtocolType.TRANSACTION_SEND_REDEEM
return this
}
public setEventTransactionRepeateRedeem(ev: EventTransactionRepeateRedeem): Event {
this.setByBasicTx(ev.userId, ev.xUserId, ev.xCommunityId, ev.transactionId, ev.amount)
this.type = EventProtocolType.TRANSACTION_REPEATE_REDEEM
return this
}
public setEventTransactionCreation(ev: EventTransactionCreation): Event {
this.setByBasicUser(ev.userId)
if (ev.transactionId) this.transactionId = ev.transactionId
if (ev.amount) this.amount = ev.amount
this.type = EventProtocolType.TRANSACTION_CREATION
return this
}
public setEventTransactionReceive(ev: EventTransactionReceive): Event {
this.setByBasicTx(ev.userId, ev.xUserId, ev.xCommunityId, ev.transactionId, ev.amount)
this.type = EventProtocolType.TRANSACTION_RECEIVE
return this
}
public setEventTransactionReceiveRedeem(ev: EventTransactionReceiveRedeem): Event {
this.setByBasicTx(ev.userId, ev.xUserId, ev.xCommunityId, ev.transactionId, ev.amount)
this.type = EventProtocolType.TRANSACTION_RECEIVE_REDEEM
return this
}
public setEventContributionCreate(ev: EventContributionCreate): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CONTRIBUTION_CREATE
return this
}
public setEventContributionConfirm(ev: EventContributionConfirm): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
if (ev.xUserId) this.xUserId = ev.xUserId
if (ev.xCommunityId) this.xCommunityId = ev.xCommunityId
this.type = EventProtocolType.CONTRIBUTION_CONFIRM
return this
}
public setEventContributionLinkDefine(ev: EventContributionLinkDefine): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CONTRIBUTION_LINK_DEFINE
return this
}
public setEventContributionLinkActivateRedeem(ev: EventContributionLinkActivateRedeem): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CONTRIBUTION_LINK_ACTIVATE_REDEEM
return this
}
setByBasicUser(userId: number): Event {
this.setEventBasic()
this.userId = userId
return this
}
setByBasicTx(
userId: number,
xUserId?: number,
xCommunityId?: number,
transactionId?: number,
amount?: decimal,
): Event {
this.setByBasicUser(userId)
if (xUserId) this.xUserId = xUserId
if (xCommunityId) this.xCommunityId = xCommunityId
if (transactionId) this.transactionId = transactionId
if (amount) this.amount = amount
return this
}
setByBasicCt(userId: number, contributionId: number, amount?: decimal): Event {
this.setByBasicUser(userId)
if (contributionId) this.contributionId = contributionId
if (amount) this.amount = amount
return this
}
setByBasicRedeem(userId: number, transactionId?: number, contributionId?: number): Event {
this.setByBasicUser(userId)
if (transactionId) this.transactionId = transactionId
if (contributionId) this.contributionId = contributionId
return this
}
setByEventTransactionCreation(event: EventTransactionCreation): Event {
this.type = event.type
this.createdAt = event.createdAt
this.userId = event.userId
this.transactionId = event.transactionId
this.amount = event.amount
return this
}
setByEventContributionConfirm(event: EventContributionConfirm): Event {
this.type = event.type
this.createdAt = event.createdAt
this.userId = event.userId
this.xUserId = event.xUserId
this.xCommunityId = event.xCommunityId
this.amount = event.amount
return this
}
id: number
type: string
createdAt: Date
userId: number
xUserId?: number
xCommunityId?: number
transactionId?: number
contributionId?: number
amount?: decimal
}

View File

@ -0,0 +1,39 @@
import { Event } from '@/event/Event'
import { backendLogger as logger } from '@/server/logger'
import { EventProtocol } from '@entity/EventProtocol'
import CONFIG from '@/config'
class EventProtocolEmitter {
/* }extends EventEmitter { */
private events: Event[]
public addEvent(event: Event) {
this.events.push(event)
}
public getEvents(): Event[] {
return this.events
}
public isDisabled() {
logger.info(`EventProtocol - isDisabled=${CONFIG.EVENT_PROTOCOL_DISABLED}`)
return CONFIG.EVENT_PROTOCOL_DISABLED === true
}
public async writeEvent(event: Event): Promise<void> {
if (!eventProtocol.isDisabled()) {
logger.info(`writeEvent(${JSON.stringify(event)})`)
const dbEvent = new EventProtocol()
dbEvent.type = event.type
dbEvent.createdAt = event.createdAt
dbEvent.userId = event.userId
if (event.xUserId) dbEvent.xUserId = event.xUserId
if (event.xCommunityId) dbEvent.xCommunityId = event.xCommunityId
if (event.contributionId) dbEvent.contributionId = event.contributionId
if (event.transactionId) dbEvent.transactionId = event.transactionId
if (event.amount) dbEvent.amount = event.amount
await dbEvent.save()
}
}
}
export const eventProtocol = new EventProtocolEmitter()

View File

@ -0,0 +1,24 @@
export enum EventProtocolType {
BASIC = 'BASIC',
VISIT_GRADIDO = 'VISIT_GRADIDO',
REGISTER = 'REGISTER',
REDEEM_REGISTER = 'REDEEM_REGISTER',
INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL',
CONFIRM_EMAIL = 'CONFIRM_EMAIL',
REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP',
LOGIN = 'LOGIN',
REDEEM_LOGIN = 'REDEEM_LOGIN',
ACTIVATE_ACCOUNT = 'ACTIVATE_ACCOUNT',
PASSWORD_CHANGE = 'PASSWORD_CHANGE',
TRANSACTION_SEND = 'TRANSACTION_SEND',
TRANSACTION_SEND_REDEEM = 'TRANSACTION_SEND_REDEEM',
TRANSACTION_REPEATE_REDEEM = 'TRANSACTION_REPEATE_REDEEM',
TRANSACTION_CREATION = 'TRANSACTION_CREATION',
TRANSACTION_RECEIVE = 'TRANSACTION_RECEIVE',
TRANSACTION_RECEIVE_REDEEM = 'TRANSACTION_RECEIVE_REDEEM',
CONTRIBUTION_CREATE = 'CONTRIBUTION_CREATE',
CONTRIBUTION_CONFIRM = 'CONTRIBUTION_CONFIRM',
CONTRIBUTION_LINK_DEFINE = 'CONTRIBUTION_LINK_DEFINE',
CONTRIBUTION_LINK_ACTIVATE_REDEEM = 'CONTRIBUTION_LINK_ACTIVATE_REDEEM',
}

View File

@ -7,18 +7,24 @@ import { User } from './User'
export class Contribution {
constructor(contribution: dbContribution, user: User) {
this.id = contribution.id
this.user = user
this.firstName = user ? user.firstName : null
this.lastName = user ? user.lastName : null
this.amount = contribution.amount
this.memo = contribution.memo
this.createdAt = contribution.createdAt
this.deletedAt = contribution.deletedAt
this.confirmedAt = contribution.confirmedAt
this.confirmedBy = contribution.confirmedBy
}
@Field(() => Number)
id: number
@Field(() => User)
user: User
@Field(() => String, { nullable: true })
firstName: string | null
@Field(() => String, { nullable: true })
lastName: string | null
@Field(() => Decimal)
amount: Decimal
@ -31,10 +37,21 @@ export class Contribution {
@Field(() => Date, { nullable: true })
deletedAt: Date | null
@Field(() => Date, { nullable: true })
confirmedAt: Date | null
@Field(() => Number, { nullable: true })
confirmedBy: number | null
}
@ObjectType()
export class ContributionListResult {
constructor(count: number, list: Contribution[]) {
this.linkCount = count
this.linkList = list
}
@Field(() => Int)
linkCount: number

View File

@ -7,7 +7,7 @@ import {
createContribution,
updateContribution,
} from '@/seeds/graphql/mutations'
import { listContributions, login } from '@/seeds/graphql/queries'
import { listAllContributions, listContributions, login } from '@/seeds/graphql/queries'
import { cleanDB, resetToken, testEnvironment } from '@test/helpers'
import { GraphQLError } from 'graphql'
import { userFactory } from '@/seeds/factory/user'
@ -438,4 +438,82 @@ describe('ContributionResolver', () => {
})
})
})
describe('listAllContribution', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
query({
query: listAllContributions,
variables: {
currentPage: 1,
pageSize: 25,
order: 'DESC',
filterConfirmed: false,
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('authenticated', () => {
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 query({
query: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
await mutate({
mutation: createContribution,
variables: {
amount: 100.0,
memo: 'Test env contribution',
creationDate: new Date().toString(),
},
})
})
it('returns allCreation', async () => {
await expect(
query({
query: listAllContributions,
variables: {
currentPage: 1,
pageSize: 25,
order: 'DESC',
filterConfirmed: false,
},
}),
).resolves.toEqual(
expect.objectContaining({
data: {
listAllContributions: {
linkCount: 2,
linkList: expect.arrayContaining([
expect.objectContaining({
id: expect.any(Number),
memo: 'Herzlich Willkommen bei Gradido!',
amount: '1000',
}),
expect.objectContaining({
id: expect.any(Number),
memo: 'Test env contribution',
amount: '100',
}),
]),
},
},
}),
)
})
})
})
})

View File

@ -7,7 +7,7 @@ import { FindOperator, IsNull } from '@dbTools/typeorm'
import ContributionArgs from '@arg/ContributionArgs'
import Paginated from '@arg/Paginated'
import { Order } from '@enum/Order'
import { Contribution } from '@model/Contribution'
import { Contribution, ContributionListResult } from '@model/Contribution'
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
import { User } from '@model/User'
import { validateContribution, getUserCreation, updateCreations } from './util/creations'
@ -58,12 +58,35 @@ export class ContributionResolver {
order: {
createdAt: order,
},
withDeleted: true,
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
return contributions.map((contribution) => new Contribution(contribution, new User(user)))
}
@Authorized([RIGHTS.LIST_ALL_CONTRIBUTIONS])
@Query(() => ContributionListResult)
async listAllContributions(
@Args()
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
): Promise<ContributionListResult> {
const [dbContributions, count] = await dbContribution.findAndCount({
relations: ['user'],
order: {
createdAt: order,
},
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
return new ContributionListResult(
count,
dbContributions.map(
(contribution) => new Contribution(contribution, new User(contribution.user)),
),
)
}
@Authorized([RIGHTS.UPDATE_CONTRIBUTION])
@Mutation(() => UnconfirmedContribution)
async updateContribution(

View File

@ -23,6 +23,14 @@ import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegi
import { klicktippSignIn } from '@/apis/KlicktippController'
import { RIGHTS } from '@/auth/RIGHTS'
import { hasElopageBuys } from '@/util/hasElopageBuys'
import { eventProtocol } from '@/event/EventProtocolEmitter'
import {
Event,
EventLogin,
EventRedeemRegister,
EventRegister,
EventSendConfirmationEmail,
} from '@/event/Event'
import { getUserCreation } from './util/creations'
// eslint-disable-next-line @typescript-eslint/no-var-requires
@ -291,6 +299,9 @@ export class UserResolver {
key: 'token',
value: encode(dbUser.pubKey),
})
const ev = new EventLogin()
ev.userId = user.id
eventProtocol.writeEvent(new Event().setEventLogin(ev))
logger.info('successful Login:' + user)
return user
}
@ -368,6 +379,9 @@ export class UserResolver {
// const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
const emailHash = getEmailHash(email)
const eventRegister = new EventRegister()
const eventRedeemRegister = new EventRedeemRegister()
const eventSendConfirmEmail = new EventSendConfirmationEmail()
const dbUser = new DbUser()
dbUser.email = email
dbUser.firstName = firstName
@ -385,12 +399,14 @@ export class UserResolver {
logger.info('redeemCode found contributionLink=' + contributionLink)
if (contributionLink) {
dbUser.contributionLinkId = contributionLink.id
eventRedeemRegister.contributionId = contributionLink.id
}
} else {
const transactionLink = await dbTransactionLink.findOne({ code: redeemCode })
logger.info('redeemCode found transactionLink=' + transactionLink)
if (transactionLink) {
dbUser.referrerId = transactionLink.userId
eventRedeemRegister.transactionId = transactionLink.id
}
}
}
@ -401,6 +417,7 @@ export class UserResolver {
// loginUser.pubKey = keyPair[0]
// loginUser.privKey = encryptedPrivkey
const event = new Event()
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED')
@ -430,6 +447,9 @@ export class UserResolver {
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
})
logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`)
eventSendConfirmEmail.userId = dbUser.id
eventProtocol.writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmEmail))
/* uncomment this, when you need the activation link on the console */
// In case EMails are disabled log the activation link for the user
if (!emailSent) {
@ -446,6 +466,14 @@ export class UserResolver {
}
logger.info('createUser() successful...')
if (redeemCode) {
eventRedeemRegister.userId = dbUser.id
eventProtocol.writeEvent(event.setEventRedeemRegister(eventRedeemRegister))
} else {
eventRegister.userId = dbUser.id
eventProtocol.writeEvent(event.setEventRegister(eventRegister))
}
return new User(dbUser)
}

View File

@ -191,6 +191,24 @@ export const listContributions = gql`
}
}
`
export const listAllContributions = `
query ($currentPage: Int = 1, $pageSize: Int = 5, $order: Order = DESC) {
listAllContributions(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
linkCount
linkList {
id
firstName
lastName
amount
memo
createdAt
confirmedAt
confirmedBy
}
}
}
`
// from admin interface
export const listUnconfirmedContributions = gql`

View File

@ -31,11 +31,14 @@ const filterVariables = (variables: any) => {
const logPlugin = {
requestDidStart(requestContext: any) {
const { logger } = requestContext
const { query, mutation, variables } = requestContext.request
const { query, mutation, variables, operationName } = requestContext.request
if (operationName !== 'IntrospectionQuery') {
logger.info(`Request:
${mutation || query}variables: ${JSON.stringify(filterVariables(variables), null, 2)}`)
}
return {
willSendResponse(requestContext: any) {
if (operationName !== 'IntrospectionQuery') {
if (requestContext.context.user) logger.info(`User ID: ${requestContext.context.user.id}`)
if (requestContext.response.data) {
logger.info('Response Success!')
@ -45,6 +48,7 @@ ${JSON.stringify(requestContext.response.data, null, 2)}`)
if (requestContext.response.errors)
logger.error(`Response-Errors:
${JSON.stringify(requestContext.response.errors, null, 2)}`)
}
return requestContext
},
}

View File

@ -1,6 +1,15 @@
import Decimal from 'decimal.js-light'
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn, DeleteDateColumn } from 'typeorm'
import {
BaseEntity,
Column,
Entity,
PrimaryGeneratedColumn,
DeleteDateColumn,
JoinColumn,
ManyToOne,
} from 'typeorm'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { User } from '../User'
@Entity('contributions')
export class Contribution extends BaseEntity {
@ -10,6 +19,10 @@ export class Contribution extends BaseEntity {
@Column({ unsigned: true, nullable: false, name: 'user_id' })
userId: number
@ManyToOne(() => User, (user) => user.contributions)
@JoinColumn({ name: 'user_id' })
user: User
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' })
createdAt: Date

View File

@ -1,4 +1,13 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm'
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
DeleteDateColumn,
OneToMany,
JoinColumn,
} from 'typeorm'
import { Contribution } from '../Contribution'
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {
@ -76,4 +85,8 @@ export class User extends BaseEntity {
default: null,
})
passphrase: string
@OneToMany(() => Contribution, (contribution) => contribution.user)
@JoinColumn({ name: 'user_id' })
contributions?: Contribution[]
}

View File

@ -0,0 +1,39 @@
import Decimal from 'decimal.js-light'
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
@Entity('event_protocol')
export class EventProtocol extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ length: 100, nullable: false, collation: 'utf8mb4_unicode_ci' })
type: string
@Column({ name: 'created_at', type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
createdAt: Date
@Column({ name: 'user_id', unsigned: true, nullable: false })
userId: number
@Column({ name: 'x_user_id', unsigned: true, nullable: true })
xUserId: number
@Column({ name: 'x_community_id', unsigned: true, nullable: true })
xCommunityId: number
@Column({ name: 'transaction_id', unsigned: true, nullable: true })
transactionId: number
@Column({ name: 'contribution_id', unsigned: true, nullable: true })
contributionId: number
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: true,
transformer: DecimalTransformer,
})
amount: Decimal
}

View File

@ -0,0 +1 @@
export { EventProtocol } from './0043-add_event_protocol_table/EventProtocol'

View File

@ -6,6 +6,7 @@ import { Transaction } from './Transaction'
import { TransactionLink } from './TransactionLink'
import { User } from './User'
import { Contribution } from './Contribution'
import { EventProtocol } from './EventProtocol'
export const entities = [
Contribution,
@ -16,4 +17,5 @@ export const entities = [
Transaction,
TransactionLink,
User,
EventProtocol,
]

View File

@ -0,0 +1,28 @@
/* MIGRATION TO ADD EVENT_PROTOCOL
*
* This migration adds the table `event_protocol` in order to store all sorts of business event data
*/
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`
CREATE TABLE IF NOT EXISTS \`event_protocol\` (
\`id\` int(10) unsigned NOT NULL AUTO_INCREMENT,
\`type\` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
\`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
\`user_id\` int(10) unsigned NOT NULL,
\`x_user_id\` int(10) unsigned NULL DEFAULT NULL,
\`x_community_id\` int(10) unsigned NULL DEFAULT NULL,
\`transaction_id\` int(10) unsigned NULL DEFAULT NULL,
\`contribution_id\` int(10) unsigned NULL DEFAULT NULL,
\`amount\` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// write downgrade logic as parameter of queryFn
await queryFn(`DROP TABLE IF EXISTS \`event_protocol\`;`)
}

View File

@ -26,7 +26,7 @@ COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code}
COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
# backend
BACKEND_CONFIG_VERSION=v8.2022-06-20
BACKEND_CONFIG_VERSION=v9.2022-07-07
JWT_EXPIRES_IN=10m
GDT_API_URL=https://gdt.gradido.net
@ -53,6 +53,10 @@ EMAIL_CODE_REQUEST_TIME=10
WEBHOOK_ELOPAGE_SECRET=secret
# EventProtocol
EVENT_PROTOCOL_DISABLED=false
# database
DATABASE_CONFIG_VERSION=v1.2022-03-18

View File

@ -7,7 +7,7 @@ With the business event protocol the gradido application will capture and persis
The different event types will be defined as Enum. The following list is a first draft and will grow with further event types in the future.
| EventType | Value | Description |
| --------------------------- | ----- | ---------------------------------------------------------------------------------------------------- |
| ----------------------------------- | ----- | ------------------------------------------------------------------------------------------------------ |
| BasicEvent | 0 | the basic event is the root of all further extending event types |
| VisitGradidoEvent | 10 | if a user visits a gradido page without login or register |
| RegisterEvent | 20 | the user presses the register button |
@ -20,15 +20,16 @@ The different event types will be defined as Enum. The following list is a first
| RedeemLoginEvent | 31 | the user presses the login button initiated by the redeem link |
| ActivateAccountEvent | 32 | the system activates the users account during the first login process |
| PasswordChangeEvent | 33 | the user changes his password |
| TxSendEvent | 40 | the user creates a transaction and sends it online |
| TxSendRedeemEvent | 41 | the user creates a transaction and sends it per redeem link |
| TxRepeateRedeemEvent | 42 | the user recreates a redeem link of a still open transaction |
| TxCreationEvent | 50 | the user receives a creation transaction for his confirmed contribution |
| TxReceiveEvent | 51 | the user receives a transaction from an other user and posts the amount on his account |
| TxReceiveRedeemEvent | 52 | the user activates the redeem link and receives the transaction and posts the amount on his account |
| ContribCreateEvent | 60 | the user enters his contribution and asks for confirmation |
| ContribConfirmEvent | 61 | the user confirms a contribution of an other user (for future multi confirmation from several users) |
| | | |
| TransactionSendEvent | 40 | the user creates a transaction and sends it online |
| TransactionSendRedeemEvent | 41 | the user creates a transaction and sends it per redeem link |
| TransactionRepeateRedeemEvent | 42 | the user recreates a redeem link of a still open transaction |
| TransactionCreationEvent | 50 | the user receives a creation transaction for his confirmed contribution |
| TransactionReceiveEvent | 51 | the user receives a transaction from an other user and posts the amount on his account |
| TransactionReceiveRedeemEvent | 52 | the user activates the redeem link and receives the transaction and posts the amount on his account |
| ContributionCreateEvent | 60 | the user enters his contribution and asks for confirmation |
| ContributionConfirmEvent | 61 | the user confirms a contribution of an other user (for future multi confirmation from several users) |
| ContributionLinkDefineEvent | 70 | the admin user defines a contributionLink, which could be send per Link/QR-Code on an other medium |
| ContributionLinkActivateRedeemEvent | 71 | the user activates a received contributionLink to create a contribution entry for the contributionLink |
## EventProtocol - Entity
@ -51,28 +52,29 @@ The business events will be stored in database in the new table `EventProtocol`.
The following table lists for each event type the mandatory attributes, which have to be initialized at event occurence and to be written in the database event protocol table:
| EventType | id | type | createdAt | userID | XuserID | XCommunityID | transactionID | contribID | amount |
| :-------------------------- | :-: | :--: | :-------: | :----: | :-----: | :----------: | :-----------: | :-------: | :----: |
| :---------------------------------- | :-: | :--: | :-------: | :----: | :-----: | :----------: | :-----------: | :-------: | :----: |
| BasicEvent | x | x | x | | | | | | |
| VisitGradidoEvent | x | x | x | | | | | | |
| RegisterEvent | x | x | x | x | | | | | |
| RedeemRegisterEvent | x | x | x | x | | | | | |
| RedeemRegisterEvent | x | x | x | x | | | (x) | (x) | |
| InActiveAccountEvent | x | x | x | x | | | | | |
| SendConfirmEmailEvent | x | x | x | x | | | | | |
| ConfirmEmailEvent | x | x | x | x | | | | | |
| RegisterEmailKlickTippEvent | x | x | x | x | | | | | |
| LoginEvent | x | x | x | x | | | | | |
| RedeemLoginEvent | x | x | x | x | | | | | |
| RedeemLoginEvent | x | x | x | x | | | (x) | (x) | |
| ActivateAccountEvent | x | x | x | x | | | | | |
| PasswordChangeEvent | x | x | x | x | | | | | |
| TxSendEvent | x | x | x | x | x | x | x | | x |
| TxSendRedeemEvent | x | x | x | x | x | x | x | | x |
| TxRepeateRedeemEvent | x | x | x | x | x | x | x | | x |
| TxCreationEvent | x | x | x | x | | | x | | x |
| TxReceiveEvent | x | x | x | x | x | x | x | | x |
| TxReceiveRedeemEvent | x | x | x | x | x | x | x | | x |
| ContribCreateEvent | x | x | x | x | | | | x | |
| ContribConfirmEvent | x | x | x | x | x | x | | x | |
| | | | | | | | | | |
| TransactionSendEvent | x | x | x | x | x | x | x | | x |
| TransactionSendRedeemEvent | x | x | x | x | x | x | x | | x |
| TransactionRepeateRedeemEvent | x | x | x | x | x | x | x | | x |
| TransactionCreationEvent | x | x | x | x | | | x | | x |
| TransactionReceiveEvent | x | x | x | x | x | x | x | | x |
| TransactionReceiveRedeemEvent | x | x | x | x | x | x | x | | x |
| ContributionCreateEvent | x | x | x | x | | | | x | x |
| ContributionConfirmEvent | x | x | x | x | x | x | | x | x |
| ContributionLinkDefineEvent | x | x | x | x | | | | | x |
| ContributionLinkActivateRedeemEvent | x | x | x | x | | | | x | x |
## Event creation

View File

@ -13,6 +13,7 @@ export const login = gql`
hasElopage
publisherId
isAdmin
creation
}
}
`
@ -30,6 +31,7 @@ export const verifyLogin = gql`
hasElopage
publisherId
isAdmin
creation
}
}
`

View File

@ -47,6 +47,9 @@ export const mutations = {
hasElopage: (state, hasElopage) => {
state.hasElopage = hasElopage
},
creation: (state, creation) => {
state.creation = creation
},
}
export const actions = {
@ -60,6 +63,7 @@ export const actions = {
commit('hasElopage', data.hasElopage)
commit('publisherId', data.publisherId)
commit('isAdmin', data.isAdmin)
commit('creation', data.creation)
},
logout: ({ commit, state }) => {
commit('token', null)
@ -71,6 +75,7 @@ export const actions = {
commit('hasElopage', false)
commit('publisherId', null)
commit('isAdmin', false)
commit('creation', null)
localStorage.clear()
},
}
@ -96,6 +101,7 @@ try {
newsletterState: null,
hasElopage: false,
publisherId: null,
creation: null,
},
getters: {},
// Syncronous mutation of the state

View File

@ -30,6 +30,7 @@ const {
publisherId,
isAdmin,
hasElopage,
creation,
} = mutations
const { login, logout } = actions
@ -139,6 +140,14 @@ describe('Vuex store', () => {
expect(state.hasElopage).toBeTruthy()
})
})
describe('creation', () => {
it('sets the state of creation', () => {
const state = { creation: null }
creation(state, true)
expect(state.creation).toEqual(true)
})
})
})
describe('actions', () => {
@ -156,11 +165,12 @@ describe('Vuex store', () => {
hasElopage: false,
publisherId: 1234,
isAdmin: true,
creation: ['1000', '1000', '1000'],
}
it('calls nine commits', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenCalledTimes(8)
expect(commit).toHaveBeenCalledTimes(9)
})
it('commits email', () => {
@ -202,6 +212,11 @@ describe('Vuex store', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(8, 'isAdmin', true)
})
it('commits creation', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(9, 'creation', ['1000', '1000', '1000'])
})
})
describe('logout', () => {
@ -210,7 +225,7 @@ describe('Vuex store', () => {
it('calls nine commits', () => {
logout({ commit, state })
expect(commit).toHaveBeenCalledTimes(8)
expect(commit).toHaveBeenCalledTimes(9)
})
it('commits token', () => {
@ -253,6 +268,11 @@ describe('Vuex store', () => {
expect(commit).toHaveBeenNthCalledWith(8, 'isAdmin', false)
})
it('commits creation', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(9, 'creation', null)
})
// how to get this working?
it.skip('calls localStorage.clear()', () => {
const clearStorageMock = jest.fn()