diff --git a/backend/.env.dist b/backend/.env.dist index 780e60e5a..ae89985e3 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -1,57 +1,60 @@ -CONFIG_VERSION=v8.2022-06-20 - -# Server -PORT=4000 -JWT_SECRET=secret123 -JWT_EXPIRES_IN=10m -GRAPHIQL=false -GDT_API_URL=https://gdt.gradido.net - -# Database -DB_HOST=localhost -DB_PORT=3306 -DB_USER=root -DB_PASSWORD= -DB_DATABASE=gradido_community -TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log - -# Klicktipp -KLICKTIPP=false -KLICKTTIPP_API_URL=https://api.klicktipp.com -KLICKTIPP_USER=gradido_test -KLICKTIPP_PASSWORD=secret321 -KLICKTIPP_APIKEY_DE=SomeFakeKeyDE -KLICKTIPP_APIKEY_EN=SomeFakeKeyEN - -# Community -COMMUNITY_NAME=Gradido Entwicklung -COMMUNITY_URL=http://localhost/ -COMMUNITY_REGISTER_URL=http://localhost/register -COMMUNITY_REDEEM_URL=http://localhost/redeem/{code} -COMMUNITY_REDEEM_CONTRIBUTION_URL=http://localhost/redeem/CL-{code} -COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido. - -# Login Server -LOGIN_APP_SECRET=21ffbbc616fe -LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a - -# EMail -EMAIL=false -EMAIL_USERNAME=gradido_email -EMAIL_SENDER=info@gradido.net -EMAIL_PASSWORD=xxx -EMAIL_SMTP_URL=gmail.com -EMAIL_SMTP_PORT=587 -EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{optin}{code} -EMAIL_LINK_SETPASSWORD=http://localhost/reset-password/{optin} -EMAIL_LINK_FORGOTPASSWORD=http://localhost/forgot-password -EMAIL_LINK_OVERVIEW=http://localhost/overview -EMAIL_CODE_VALID_TIME=1440 -EMAIL_CODE_REQUEST_TIME=10 - -# Webhook -WEBHOOK_ELOPAGE_SECRET=secret - -# SET LOG LEVEL AS NEEDED IN YOUR .ENV -# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal -# LOG_LEVEL=info +CONFIG_VERSION=v9.2022-07-07 + +# Server +PORT=4000 +JWT_SECRET=secret123 +JWT_EXPIRES_IN=10m +GRAPHIQL=false +GDT_API_URL=https://gdt.gradido.net + +# Database +DB_HOST=localhost +DB_PORT=3306 +DB_USER=root +DB_PASSWORD= +DB_DATABASE=gradido_community +TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log + +# Klicktipp +KLICKTIPP=false +KLICKTTIPP_API_URL=https://api.klicktipp.com +KLICKTIPP_USER=gradido_test +KLICKTIPP_PASSWORD=secret321 +KLICKTIPP_APIKEY_DE=SomeFakeKeyDE +KLICKTIPP_APIKEY_EN=SomeFakeKeyEN + +# Community +COMMUNITY_NAME=Gradido Entwicklung +COMMUNITY_URL=http://localhost/ +COMMUNITY_REGISTER_URL=http://localhost/register +COMMUNITY_REDEEM_URL=http://localhost/redeem/{code} +COMMUNITY_REDEEM_CONTRIBUTION_URL=http://localhost/redeem/CL-{code} +COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido. + +# Login Server +LOGIN_APP_SECRET=21ffbbc616fe +LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a + +# EMail +EMAIL=false +EMAIL_USERNAME=gradido_email +EMAIL_SENDER=info@gradido.net +EMAIL_PASSWORD=xxx +EMAIL_SMTP_URL=gmail.com +EMAIL_SMTP_PORT=587 +EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{optin}{code} +EMAIL_LINK_SETPASSWORD=http://localhost/reset-password/{optin} +EMAIL_LINK_FORGOTPASSWORD=http://localhost/forgot-password +EMAIL_LINK_OVERVIEW=http://localhost/overview +EMAIL_CODE_VALID_TIME=1440 +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 diff --git a/backend/.env.template b/backend/.env.template index 94bf41550..beaa256ef 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -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 diff --git a/backend/README.md b/backend/README.md index e74750c46..b27ab16d9 100644 --- a/backend/README.md +++ b/backend/README.md @@ -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. diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index 1d0fd2856..639326a64 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -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', diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts index 75e552e79..935bd94e5 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -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 diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 8b84c059d..2120fce71 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,123 +1,129 @@ -// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env) - -import dotenv from 'dotenv' -import Decimal from 'decimal.js-light' -dotenv.config() - -Decimal.set({ - precision: 25, - rounding: Decimal.ROUND_HALF_UP, -}) - -const constants = { - DB_VERSION: '0042-update_transactions_for_blockchain', - 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', - CURRENT: '', - }, -} - -const server = { - PORT: process.env.PORT || 4000, - JWT_SECRET: process.env.JWT_SECRET || 'secret123', - JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m', - GRAPHIQL: process.env.GRAPHIQL === 'true' || false, - GDT_API_URL: process.env.GDT_API_URL || 'https://gdt.gradido.net', - PRODUCTION: process.env.NODE_ENV === 'production' || false, -} - -const database = { - DB_HOST: process.env.DB_HOST || 'localhost', - DB_PORT: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306, - DB_USER: process.env.DB_USER || 'root', - DB_PASSWORD: process.env.DB_PASSWORD || '', - DB_DATABASE: process.env.DB_DATABASE || 'gradido_community', - TYPEORM_LOGGING_RELATIVE_PATH: process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.backend.log', -} - -const klicktipp = { - KLICKTIPP: process.env.KLICKTIPP === 'true' || false, - KLICKTTIPP_API_URL: process.env.KLICKTIPP_API_URL || 'https://api.klicktipp.com', - KLICKTIPP_USER: process.env.KLICKTIPP_USER || 'gradido_test', - KLICKTIPP_PASSWORD: process.env.KLICKTIPP_PASSWORD || 'secret321', - KLICKTIPP_APIKEY_DE: process.env.KLICKTIPP_APIKEY_DE || 'SomeFakeKeyDE', - KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN || 'SomeFakeKeyEN', -} - -const community = { - COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung', - COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/', - COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register', - COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL || 'http://localhost/redeem/{code}', - COMMUNITY_REDEEM_CONTRIBUTION_URL: - process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}', - COMMUNITY_DESCRIPTION: - process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.', -} - -const loginServer = { - LOGIN_APP_SECRET: process.env.LOGIN_APP_SECRET || '21ffbbc616fe', - LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY || 'a51ef8ac7ef1abf162fb7a65261acd7a', -} - -const email = { - EMAIL: process.env.EMAIL === 'true' || false, - EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email', - EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net', - EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx', - EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'gmail.com', - EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT || '587', - EMAIL_LINK_VERIFICATION: - process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{optin}{code}', - EMAIL_LINK_SETPASSWORD: - process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{optin}', - EMAIL_LINK_FORGOTPASSWORD: - process.env.EMAIL_LINK_FORGOTPASSWORD || 'http://localhost/forgot-password', - EMAIL_LINK_OVERVIEW: process.env.EMAIL_LINK_OVERVIEW || 'http://localhost/overview', - // time in minutes a optin code is valid - EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME - ? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 1440 - : 1440, - // time in minutes that must pass to request a new optin code - EMAIL_CODE_REQUEST_TIME: process.env.EMAIL_CODE_REQUEST_TIME - ? parseInt(process.env.EMAIL_CODE_REQUEST_TIME) || 10 - : 10, -} - -const webhook = { - // Elopage - WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret', -} - -// This is needed by graphql-directive-auth -process.env.APP_SECRET = server.JWT_SECRET - -// Check config version -constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT -if ( - ![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes( - constants.CONFIG_VERSION.CURRENT, - ) -) { - throw new Error( - `Fatal: Config Version incorrect - expected "${constants.CONFIG_VERSION.EXPECTED}" or "${constants.CONFIG_VERSION.DEFAULT}", but found "${constants.CONFIG_VERSION.CURRENT}"`, - ) -} - -const CONFIG = { - ...constants, - ...server, - ...database, - ...klicktipp, - ...community, - ...email, - ...loginServer, - ...webhook, -} - -export default CONFIG +// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env) + +import dotenv from 'dotenv' +import Decimal from 'decimal.js-light' +dotenv.config() + +Decimal.set({ + precision: 25, + rounding: Decimal.ROUND_HALF_UP, +}) + +const constants = { + 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: 'v9.2022-07-07', + CURRENT: '', + }, +} + +const server = { + PORT: process.env.PORT || 4000, + JWT_SECRET: process.env.JWT_SECRET || 'secret123', + JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m', + GRAPHIQL: process.env.GRAPHIQL === 'true' || false, + GDT_API_URL: process.env.GDT_API_URL || 'https://gdt.gradido.net', + PRODUCTION: process.env.NODE_ENV === 'production' || false, +} + +const database = { + DB_HOST: process.env.DB_HOST || 'localhost', + DB_PORT: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306, + DB_USER: process.env.DB_USER || 'root', + DB_PASSWORD: process.env.DB_PASSWORD || '', + DB_DATABASE: process.env.DB_DATABASE || 'gradido_community', + TYPEORM_LOGGING_RELATIVE_PATH: process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.backend.log', +} + +const klicktipp = { + KLICKTIPP: process.env.KLICKTIPP === 'true' || false, + KLICKTTIPP_API_URL: process.env.KLICKTIPP_API_URL || 'https://api.klicktipp.com', + KLICKTIPP_USER: process.env.KLICKTIPP_USER || 'gradido_test', + KLICKTIPP_PASSWORD: process.env.KLICKTIPP_PASSWORD || 'secret321', + KLICKTIPP_APIKEY_DE: process.env.KLICKTIPP_APIKEY_DE || 'SomeFakeKeyDE', + KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN || 'SomeFakeKeyEN', +} + +const community = { + COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung', + COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/', + COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register', + COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL || 'http://localhost/redeem/{code}', + COMMUNITY_REDEEM_CONTRIBUTION_URL: + process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}', + COMMUNITY_DESCRIPTION: + process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.', +} + +const loginServer = { + LOGIN_APP_SECRET: process.env.LOGIN_APP_SECRET || '21ffbbc616fe', + LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY || 'a51ef8ac7ef1abf162fb7a65261acd7a', +} + +const email = { + EMAIL: process.env.EMAIL === 'true' || false, + EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email', + EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net', + EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx', + EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'gmail.com', + EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT || '587', + EMAIL_LINK_VERIFICATION: + process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{optin}{code}', + EMAIL_LINK_SETPASSWORD: + process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{optin}', + EMAIL_LINK_FORGOTPASSWORD: + process.env.EMAIL_LINK_FORGOTPASSWORD || 'http://localhost/forgot-password', + EMAIL_LINK_OVERVIEW: process.env.EMAIL_LINK_OVERVIEW || 'http://localhost/overview', + // time in minutes a optin code is valid + EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME + ? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 1440 + : 1440, + // time in minutes that must pass to request a new optin code + EMAIL_CODE_REQUEST_TIME: process.env.EMAIL_CODE_REQUEST_TIME + ? parseInt(process.env.EMAIL_CODE_REQUEST_TIME) || 10 + : 10, +} + +const webhook = { + // Elopage + 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 + +// Check config version +constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT +if ( + ![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes( + constants.CONFIG_VERSION.CURRENT, + ) +) { + throw new Error( + `Fatal: Config Version incorrect - expected "${constants.CONFIG_VERSION.EXPECTED}" or "${constants.CONFIG_VERSION.DEFAULT}", but found "${constants.CONFIG_VERSION.CURRENT}"`, + ) +} + +const CONFIG = { + ...constants, + ...server, + ...database, + ...klicktipp, + ...community, + ...email, + ...loginServer, + ...webhook, + ...eventProtocol, +} + +export default CONFIG diff --git a/backend/src/event/Event.ts b/backend/src/event/Event.ts new file mode 100644 index 000000000..6f07661f1 --- /dev/null +++ b/backend/src/event/Event.ts @@ -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 +} diff --git a/backend/src/event/EventProtocolEmitter.ts b/backend/src/event/EventProtocolEmitter.ts new file mode 100644 index 000000000..6dea99810 --- /dev/null +++ b/backend/src/event/EventProtocolEmitter.ts @@ -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 { + 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() diff --git a/backend/src/event/EventProtocolType.ts b/backend/src/event/EventProtocolType.ts new file mode 100644 index 000000000..0f61f787a --- /dev/null +++ b/backend/src/event/EventProtocolType.ts @@ -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', +} diff --git a/backend/src/graphql/model/Contribution.ts b/backend/src/graphql/model/Contribution.ts index dc1dd39e9..34bffd6d7 100644 --- a/backend/src/graphql/model/Contribution.ts +++ b/backend/src/graphql/model/Contribution.ts @@ -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 diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 7afae08f6..e6478ffc2 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -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', + }), + ]), + }, + }, + }), + ) + }) + }) + }) }) diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index 27f43b666..d71ecac59 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -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 { + 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( diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 5b824b38f..a89a8cb0b 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -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) } diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index c27ecdd66..deae5f97b 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -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` diff --git a/backend/src/server/plugins.ts b/backend/src/server/plugins.ts index 1972bc1c8..24df45baa 100644 --- a/backend/src/server/plugins.ts +++ b/backend/src/server/plugins.ts @@ -31,20 +31,24 @@ const filterVariables = (variables: any) => { const logPlugin = { requestDidStart(requestContext: any) { const { logger } = requestContext - const { query, mutation, variables } = requestContext.request - logger.info(`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 (requestContext.context.user) logger.info(`User ID: ${requestContext.context.user.id}`) - if (requestContext.response.data) { - logger.info('Response Success!') - logger.trace(`Response-Data: + if (operationName !== 'IntrospectionQuery') { + if (requestContext.context.user) logger.info(`User ID: ${requestContext.context.user.id}`) + if (requestContext.response.data) { + logger.info('Response Success!') + logger.trace(`Response-Data: ${JSON.stringify(requestContext.response.data, null, 2)}`) - } - if (requestContext.response.errors) - logger.error(`Response-Errors: + } + if (requestContext.response.errors) + logger.error(`Response-Errors: ${JSON.stringify(requestContext.response.errors, null, 2)}`) + } return requestContext }, } diff --git a/database/entity/0039-contributions_table/Contribution.ts b/database/entity/0039-contributions_table/Contribution.ts index 6c7358f90..b5e6ac0e0 100644 --- a/database/entity/0039-contributions_table/Contribution.ts +++ b/database/entity/0039-contributions_table/Contribution.ts @@ -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 diff --git a/database/entity/0040-add_contribution_link_id_to_user/User.ts b/database/entity/0040-add_contribution_link_id_to_user/User.ts index 9bf76e5f5..56047345a 100644 --- a/database/entity/0040-add_contribution_link_id_to_user/User.ts +++ b/database/entity/0040-add_contribution_link_id_to_user/User.ts @@ -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[] } diff --git a/database/entity/0043-add_event_protocol_table/EventProtocol.ts b/database/entity/0043-add_event_protocol_table/EventProtocol.ts new file mode 100644 index 000000000..72470d2ed --- /dev/null +++ b/database/entity/0043-add_event_protocol_table/EventProtocol.ts @@ -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 +} diff --git a/database/entity/EventProtocol.ts b/database/entity/EventProtocol.ts new file mode 100644 index 000000000..4a73f146c --- /dev/null +++ b/database/entity/EventProtocol.ts @@ -0,0 +1 @@ +export { EventProtocol } from './0043-add_event_protocol_table/EventProtocol' diff --git a/database/entity/index.ts b/database/entity/index.ts index 266c40740..733c99a3a 100644 --- a/database/entity/index.ts +++ b/database/entity/index.ts @@ -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, ] diff --git a/database/migrations/0043-add_event_protocol_table.ts b/database/migrations/0043-add_event_protocol_table.ts new file mode 100644 index 000000000..c3669f857 --- /dev/null +++ b/database/migrations/0043-add_event_protocol_table.ts @@ -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>) { + 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>) { + // write downgrade logic as parameter of queryFn + await queryFn(`DROP TABLE IF EXISTS \`event_protocol\`;`) +} diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index edb878040..d53ffebbe 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -1,80 +1,84 @@ -GRADIDO_LOG_PATH=/home/gradido/gradido/deployment/bare_metal/log - -# start script -DEPLOY_SEED_DATA=false - -# nginx -NGINX_REWRITE_LEGACY_URLS=true -NGINX_SSL=true -NGINX_SERVER_NAME=stage1.gradido.net -NGINX_SSL_CERTIFICATE=/etc/letsencrypt/live/stage1.gradido.net/fullchain.pem -NGINX_SSL_CERTIFICATE_KEY=/etc/letsencrypt/live/stage1.gradido.net/privkey.pem -NGINX_SSL_DHPARAM=/etc/letsencrypt/ssl-dhparams.pem -NGINX_SSL_INCLUDE=/etc/letsencrypt/options-ssl-nginx.conf -NGINX_UPDATE_PAGE_ROOT=/home/gradido/gradido/deployment/bare_metal/nginx/update-page - -# webhook -WEBHOOK_GITHUB_SECRET=secret -WEBHOOK_GITHUB_BRANCH=master - -# community -COMMUNITY_NAME="Gradido Development Stage1" -COMMUNITY_URL=https://stage1.gradido.net/ -COMMUNITY_REGISTER_URL=https://stage1.gradido.net/register -COMMUNITY_REDEEM_URL=https://stage1.gradido.net/redeem/{code} -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 - -JWT_EXPIRES_IN=10m -GDT_API_URL=https://gdt.gradido.net - -TYPEORM_LOGGING_RELATIVE_PATH=../deployment/bare_metal/log/typeorm.backend.log - -KLICKTIPP=false -KLICKTIPP_USER= -KLICKTIPP_PASSWORD= -KLICKTIPP_APIKEY_DE= -KLICKTIPP_APIKEY_EN= - -EMAIL=true -EMAIL_USERNAME=peter@lustig.de -EMAIL_SENDER=peter@lustig.de -EMAIL_PASSWORD=1234 -EMAIL_SMTP_URL=smtp.lustig.de -EMAIL_LINK_VERIFICATION=https://stage1.gradido.net/checkEmail/{optin}{code} -EMAIL_LINK_SETPASSWORD=https://stage1.gradido.net/reset-password/{optin} -EMAIL_LINK_FORGOTPASSWORD=https://stage1.gradido.net/forgot-password -EMAIL_LINK_OVERVIEW=https://stage1.gradido.net/overview -EMAIL_CODE_VALID_TIME=1440 -EMAIL_CODE_REQUEST_TIME=10 - -WEBHOOK_ELOPAGE_SECRET=secret - -# database -DATABASE_CONFIG_VERSION=v1.2022-03-18 - -# frontend -FRONTEND_CONFIG_VERSION=v2.2022-04-07 - -GRAPHQL_URI=https://stage1.gradido.net/graphql -ADMIN_AUTH_URL=https://stage1.gradido.net/admin/authenticate?token={token} - -DEFAULT_PUBLISHER_ID=2896 - -META_URL=http://localhost -META_TITLE_DE="Gradido – Dein Dankbarkeitskonto" -META_TITLE_EN="Gradido - Your gratitude account" -META_DESCRIPTION_DE="Dankbarkeit ist die Währung der neuen Zeit. Immer mehr Menschen entfalten ihr Potenzial und gestalten eine gute Zukunft für alle." -META_DESCRIPTION_EN="Gratitude is the currency of the new age. More and more people are unleashing their potential and shaping a good future for all." -META_KEYWORDS_DE="Grundeinkommen, Währung, Dankbarkeit, Schenk-Ökonomie, Natürliche Ökonomie des Lebens, Ökonomie, Ökologie, Potenzialentfaltung, Schenken und Danken, Kreislauf des Lebens, Geldsystem" -META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System" -META_AUTHOR="Bernd Hückstädt - Gradido-Akademie" - -# admin -ADMIN_CONFIG_VERSION=v1.2022-03-18 - -WALLET_AUTH_URL=https://stage1.gradido.net/authenticate?token={token} +GRADIDO_LOG_PATH=/home/gradido/gradido/deployment/bare_metal/log + +# start script +DEPLOY_SEED_DATA=false + +# nginx +NGINX_REWRITE_LEGACY_URLS=true +NGINX_SSL=true +NGINX_SERVER_NAME=stage1.gradido.net +NGINX_SSL_CERTIFICATE=/etc/letsencrypt/live/stage1.gradido.net/fullchain.pem +NGINX_SSL_CERTIFICATE_KEY=/etc/letsencrypt/live/stage1.gradido.net/privkey.pem +NGINX_SSL_DHPARAM=/etc/letsencrypt/ssl-dhparams.pem +NGINX_SSL_INCLUDE=/etc/letsencrypt/options-ssl-nginx.conf +NGINX_UPDATE_PAGE_ROOT=/home/gradido/gradido/deployment/bare_metal/nginx/update-page + +# webhook +WEBHOOK_GITHUB_SECRET=secret +WEBHOOK_GITHUB_BRANCH=master + +# community +COMMUNITY_NAME="Gradido Development Stage1" +COMMUNITY_URL=https://stage1.gradido.net/ +COMMUNITY_REGISTER_URL=https://stage1.gradido.net/register +COMMUNITY_REDEEM_URL=https://stage1.gradido.net/redeem/{code} +COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code} +COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community" + +# backend +BACKEND_CONFIG_VERSION=v9.2022-07-07 + +JWT_EXPIRES_IN=10m +GDT_API_URL=https://gdt.gradido.net + +TYPEORM_LOGGING_RELATIVE_PATH=../deployment/bare_metal/log/typeorm.backend.log + +KLICKTIPP=false +KLICKTIPP_USER= +KLICKTIPP_PASSWORD= +KLICKTIPP_APIKEY_DE= +KLICKTIPP_APIKEY_EN= + +EMAIL=true +EMAIL_USERNAME=peter@lustig.de +EMAIL_SENDER=peter@lustig.de +EMAIL_PASSWORD=1234 +EMAIL_SMTP_URL=smtp.lustig.de +EMAIL_LINK_VERIFICATION=https://stage1.gradido.net/checkEmail/{optin}{code} +EMAIL_LINK_SETPASSWORD=https://stage1.gradido.net/reset-password/{optin} +EMAIL_LINK_FORGOTPASSWORD=https://stage1.gradido.net/forgot-password +EMAIL_LINK_OVERVIEW=https://stage1.gradido.net/overview +EMAIL_CODE_VALID_TIME=1440 +EMAIL_CODE_REQUEST_TIME=10 + +WEBHOOK_ELOPAGE_SECRET=secret + +# EventProtocol +EVENT_PROTOCOL_DISABLED=false + + +# database +DATABASE_CONFIG_VERSION=v1.2022-03-18 + +# frontend +FRONTEND_CONFIG_VERSION=v2.2022-04-07 + +GRAPHQL_URI=https://stage1.gradido.net/graphql +ADMIN_AUTH_URL=https://stage1.gradido.net/admin/authenticate?token={token} + +DEFAULT_PUBLISHER_ID=2896 + +META_URL=http://localhost +META_TITLE_DE="Gradido – Dein Dankbarkeitskonto" +META_TITLE_EN="Gradido - Your gratitude account" +META_DESCRIPTION_DE="Dankbarkeit ist die Währung der neuen Zeit. Immer mehr Menschen entfalten ihr Potenzial und gestalten eine gute Zukunft für alle." +META_DESCRIPTION_EN="Gratitude is the currency of the new age. More and more people are unleashing their potential and shaping a good future for all." +META_KEYWORDS_DE="Grundeinkommen, Währung, Dankbarkeit, Schenk-Ökonomie, Natürliche Ökonomie des Lebens, Ökonomie, Ökologie, Potenzialentfaltung, Schenken und Danken, Kreislauf des Lebens, Geldsystem" +META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System" +META_AUTHOR="Bernd Hückstädt - Gradido-Akademie" + +# admin +ADMIN_CONFIG_VERSION=v1.2022-03-18 + +WALLET_AUTH_URL=https://stage1.gradido.net/authenticate?token={token} WALLET_URL=https://stage1.gradido.net/login \ No newline at end of file diff --git a/docu/Concepts/TechnicalRequirements/BusinessEventProtocol.md b/docu/Concepts/TechnicalRequirements/BusinessEventProtocol.md index 5a436d057..f8cf2cd03 100644 --- a/docu/Concepts/TechnicalRequirements/BusinessEventProtocol.md +++ b/docu/Concepts/TechnicalRequirements/BusinessEventProtocol.md @@ -6,29 +6,30 @@ 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 | -| RedeemRegisterEvent | 21 | the user presses the register button initiated by the redeem link | -| InActiveAccountEvent | 22 | the systems create an inactive account during the register process | -| SendConfirmEmailEvent | 23 | the system send a confirmation email to the user during the register process | -| ConfirmEmailEvent | 24 | the user confirms his email during the register process | -| RegisterEmailKlickTippEvent | 25 | the system registers the confirmed email at klicktipp | -| LoginEvent | 30 | the user presses the login button | -| 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) | -| | | | +| 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 | +| RedeemRegisterEvent | 21 | the user presses the register button initiated by the redeem link | +| InActiveAccountEvent | 22 | the systems create an inactive account during the register process | +| SendConfirmEmailEvent | 23 | the system send a confirmation email to the user during the register process | +| ConfirmEmailEvent | 24 | the user confirms his email during the register process | +| RegisterEmailKlickTippEvent | 25 | the system registers the confirmed email at klicktipp | +| LoginEvent | 30 | the user presses the login button | +| 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 | +| 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 @@ -50,29 +51,30 @@ 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 | | | | | | -| 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 | | | | | | -| 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 | | -| | | | | | | | | | | +| 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 | | | (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 | | | (x) | (x) | | +| ActivateAccountEvent | x | x | x | x | | | | | | +| PasswordChangeEvent | 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 diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index adcd653a4..27e63d568 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -13,6 +13,7 @@ export const login = gql` hasElopage publisherId isAdmin + creation } } ` @@ -30,6 +31,7 @@ export const verifyLogin = gql` hasElopage publisherId isAdmin + creation } } ` diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js index e95eec7b9..8fdbc519e 100644 --- a/frontend/src/store/store.js +++ b/frontend/src/store/store.js @@ -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 diff --git a/frontend/src/store/store.test.js b/frontend/src/store/store.test.js index 3f942fa35..651a3ccc5 100644 --- a/frontend/src/store/store.test.js +++ b/frontend/src/store/store.test.js @@ -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()