diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 3e6bafd9f..b622293ad 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0049-add_user_contacts_table', + DB_VERSION: '0050-add_messageId_to_event_protocol', 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 diff --git a/backend/src/event/Event.ts b/backend/src/event/Event.ts index 85fba896d..cec94e5bf 100644 --- a/backend/src/event/Event.ts +++ b/backend/src/event/Event.ts @@ -11,48 +11,67 @@ export class EventBasicUserId extends EventBasic { } export class EventBasicTx extends EventBasicUserId { - xUserId: number - xCommunityId: number transactionId: number amount: decimal } +export class EventBasicTxX extends EventBasicTx { + xUserId: number + xCommunityId: number +} + export class EventBasicCt extends EventBasicUserId { contributionId: number amount: decimal } +export class EventBasicCtX extends EventBasicCt { + xUserId: number + xCommunityId: number +} + export class EventBasicRedeem extends EventBasicUserId { transactionId?: number contributionId?: number } +export class EventBasicCtMsg extends EventBasicCt { + messageId: number +} + export class EventVisitGradido extends EventBasic {} export class EventRegister extends EventBasicUserId {} export class EventRedeemRegister extends EventBasicRedeem {} +export class EventVerifyRedeem extends EventBasicRedeem {} export class EventInactiveAccount extends EventBasicUserId {} export class EventSendConfirmationEmail extends EventBasicUserId {} export class EventSendAccountMultiRegistrationEmail extends EventBasicUserId {} +export class EventSendForgotPasswordEmail extends EventBasicUserId {} +export class EventSendTransactionSendEmail extends EventBasicTxX {} +export class EventSendTransactionReceiveEmail extends EventBasicTxX {} +export class EventSendTransactionLinkRedeemEmail extends EventBasicTxX {} +export class EventSendAddedContributionEmail extends EventBasicCt {} +export class EventSendContributionConfirmEmail extends EventBasicCt {} export class EventConfirmationEmail extends EventBasicUserId {} export class EventRegisterEmailKlicktipp extends EventBasicUserId {} export class EventLogin extends EventBasicUserId {} +export class EventLogout 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 EventTransactionSend extends EventBasicTxX {} +export class EventTransactionSendRedeem extends EventBasicTxX {} +export class EventTransactionRepeateRedeem extends EventBasicTxX {} +export class EventTransactionCreation extends EventBasicTx {} +export class EventTransactionReceive extends EventBasicTxX {} +export class EventTransactionReceiveRedeem extends EventBasicTxX {} export class EventContributionCreate extends EventBasicCt {} -export class EventContributionConfirm extends EventBasicCt { - xUserId: number - xCommunityId: number -} +export class EventUserCreateContributionMessage extends EventBasicCtMsg {} +export class EventAdminCreateContributionMessage extends EventBasicCtMsg {} +export class EventContributionDelete extends EventBasicCt {} +export class EventContributionUpdate extends EventBasicCt {} +export class EventContributionConfirm extends EventBasicCtX {} +export class EventContributionDeny extends EventBasicCtX {} export class EventContributionLinkDefine extends EventBasicCt {} export class EventContributionLinkActivateRedeem extends EventBasicCt {} @@ -100,6 +119,13 @@ export class Event { return this } + public setEventVerifyRedeem(ev: EventVerifyRedeem): Event { + this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId) + this.type = EventProtocolType.VERIFY_REDEEM + + return this + } + public setEventInactiveAccount(ev: EventInactiveAccount): Event { this.setByBasicUser(ev.userId) this.type = EventProtocolType.INACTIVE_ACCOUNT @@ -118,7 +144,49 @@ export class Event { ev: EventSendAccountMultiRegistrationEmail, ): Event { this.setByBasicUser(ev.userId) - this.type = EventProtocolType.SEND_ACCOUNT_MULTI_REGISTRATION_EMAIL + this.type = EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL + + return this + } + + public setEventSendForgotPasswordEmail(ev: EventSendForgotPasswordEmail): Event { + this.setByBasicUser(ev.userId) + this.type = EventProtocolType.SEND_FORGOT_PASSWORD_EMAIL + + return this + } + + public setEventSendTransactionSendEmail(ev: EventSendTransactionSendEmail): Event { + this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) + this.type = EventProtocolType.SEND_TRANSACTION_SEND_EMAIL + + return this + } + + public setEventSendTransactionReceiveEmail(ev: EventSendTransactionReceiveEmail): Event { + this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) + this.type = EventProtocolType.SEND_TRANSACTION_RECEIVE_EMAIL + + return this + } + + public setEventSendTransactionLinkRedeemEmail(ev: EventSendTransactionLinkRedeemEmail): Event { + this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) + this.type = EventProtocolType.SEND_TRANSACTION_LINK_REDEEM_EMAIL + + return this + } + + public setEventSendAddedContributionEmail(ev: EventSendAddedContributionEmail): Event { + this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) + this.type = EventProtocolType.SEND_ADDED_CONTRIBUTION_EMAIL + + return this + } + + public setEventSendContributionConfirmEmail(ev: EventSendContributionConfirmEmail): Event { + this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) + this.type = EventProtocolType.SEND_CONTRIBUTION_CONFIRM_EMAIL return this } @@ -144,6 +212,13 @@ export class Event { return this } + public setEventLogout(ev: EventLogout): Event { + this.setByBasicUser(ev.userId) + this.type = EventProtocolType.LOGOUT + + return this + } + public setEventRedeemLogin(ev: EventRedeemLogin): Event { this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId) this.type = EventProtocolType.REDEEM_LOGIN @@ -166,44 +241,42 @@ export class Event { } public setEventTransactionSend(ev: EventTransactionSend): Event { - this.setByBasicTx(ev.userId, ev.xUserId, ev.xCommunityId, ev.transactionId, ev.amount) + this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) 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.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) 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.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) 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.setByBasicTx(ev.userId, ev.transactionId, 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.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) 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.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) this.type = EventProtocolType.TRANSACTION_RECEIVE_REDEEM return this @@ -216,15 +289,48 @@ export class Event { return this } - public setEventContributionConfirm(ev: EventContributionConfirm): Event { + public setEventUserCreateContributionMessage(ev: EventUserCreateContributionMessage): Event { + this.setByBasicCtMsg(ev.userId, ev.contributionId, ev.amount, ev.messageId) + this.type = EventProtocolType.USER_CREATE_CONTRIBUTION_MESSAGE + + return this + } + + public setEventAdminCreateContributionMessage(ev: EventAdminCreateContributionMessage): Event { + this.setByBasicCtMsg(ev.userId, ev.contributionId, ev.amount, ev.messageId) + this.type = EventProtocolType.ADMIN_CREATE_CONTRIBUTION_MESSAGE + + return this + } + + public setEventContributionDelete(ev: EventContributionDelete): 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_DELETE + + return this + } + + public setEventContributionUpdate(ev: EventContributionUpdate): Event { + this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) + this.type = EventProtocolType.CONTRIBUTION_UPDATE + + return this + } + + public setEventContributionConfirm(ev: EventContributionConfirm): Event { + this.setByBasicCtX(ev.userId, ev.xUserId, ev.xCommunityId, ev.contributionId, ev.amount) this.type = EventProtocolType.CONTRIBUTION_CONFIRM return this } + public setEventContributionDeny(ev: EventContributionDeny): Event { + this.setByBasicCtX(ev.userId, ev.xUserId, ev.xCommunityId, ev.contributionId, ev.amount) + this.type = EventProtocolType.CONTRIBUTION_DENY + + return this + } + public setEventContributionLinkDefine(ev: EventContributionLinkDefine): Event { this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) this.type = EventProtocolType.CONTRIBUTION_LINK_DEFINE @@ -246,26 +352,58 @@ export class Event { return this } - setByBasicTx( - userId: number, - xUserId?: number, - xCommunityId?: number, - transactionId?: number, - amount?: decimal, - ): Event { + setByBasicTx(userId: 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 + this.transactionId = transactionId + this.amount = amount return this } - setByBasicCt(userId: number, contributionId: number, amount?: decimal): Event { + setByBasicTxX( + userId: number, + transactionId: number, + amount: decimal, + xUserId: number, + xCommunityId: number, + ): Event { + this.setByBasicTx(userId, transactionId, amount) + this.xUserId = xUserId + this.xCommunityId = xCommunityId + + return this + } + + setByBasicCt(userId: number, contributionId: number, amount: decimal): Event { this.setByBasicUser(userId) - if (contributionId) this.contributionId = contributionId - if (amount) this.amount = amount + this.contributionId = contributionId + this.amount = amount + + return this + } + + setByBasicCtMsg( + userId: number, + contributionId: number, + amount: decimal, + messageId: number, + ): Event { + this.setByBasicCt(userId, contributionId, amount) + this.messageId = messageId + + return this + } + + setByBasicCtX( + userId: number, + contributionId: number, + amount: decimal, + xUserId: number, + xCommunityId: number, + ): Event { + this.setByBasicCt(userId, contributionId, amount) + this.xUserId = xUserId + this.xCommunityId = xCommunityId return this } @@ -278,27 +416,6 @@ export class Event { 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 @@ -308,4 +425,5 @@ export class Event { transactionId?: number contributionId?: number amount?: decimal + messageId?: number } diff --git a/backend/src/event/EventProtocolType.ts b/backend/src/event/EventProtocolType.ts index 52bcf8349..d53eb6961 100644 --- a/backend/src/event/EventProtocolType.ts +++ b/backend/src/event/EventProtocolType.ts @@ -3,23 +3,36 @@ export enum EventProtocolType { VISIT_GRADIDO = 'VISIT_GRADIDO', REGISTER = 'REGISTER', REDEEM_REGISTER = 'REDEEM_REGISTER', + VERIFY_REDEEM = 'VERIFY_REDEEM', INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT', SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL', - SEND_ACCOUNT_MULTI_REGISTRATION_EMAIL = 'SEND_ACCOUNT_MULTI_REGISTRATION_EMAIL', + SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = 'SEND_ACCOUNT_MULTIREGISTRATION_EMAIL', CONFIRM_EMAIL = 'CONFIRM_EMAIL', REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP', LOGIN = 'LOGIN', + LOGOUT = 'LOGOUT', REDEEM_LOGIN = 'REDEEM_LOGIN', ACTIVATE_ACCOUNT = 'ACTIVATE_ACCOUNT', + SEND_FORGOT_PASSWORD_EMAIL = 'SEND_FORGOT_PASSWORD_EMAIL', PASSWORD_CHANGE = 'PASSWORD_CHANGE', + SEND_TRANSACTION_SEND_EMAIL = 'SEND_TRANSACTION_SEND_EMAIL', + SEND_TRANSACTION_RECEIVE_EMAIL = 'SEND_TRANSACTION_RECEIVE_EMAIL', 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', + SEND_TRANSACTION_LINK_REDEEM_EMAIL = 'SEND_TRANSACTION_LINK_REDEEM_EMAIL', + SEND_ADDED_CONTRIBUTION_EMAIL = 'SEND_ADDED_CONTRIBUTION_EMAIL', + SEND_CONTRIBUTION_CONFIRM_EMAIL = 'SEND_CONTRIBUTION_CONFIRM_EMAIL', CONTRIBUTION_CREATE = 'CONTRIBUTION_CREATE', CONTRIBUTION_CONFIRM = 'CONTRIBUTION_CONFIRM', + CONTRIBUTION_DENY = 'CONTRIBUTION_DENY', CONTRIBUTION_LINK_DEFINE = 'CONTRIBUTION_LINK_DEFINE', CONTRIBUTION_LINK_ACTIVATE_REDEEM = 'CONTRIBUTION_LINK_ACTIVATE_REDEEM', + CONTRIBUTION_DELETE = 'CONTRIBUTION_DELETE', + CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE', + USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE', + ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE', } diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index 20f11ff9a..199fb6c76 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -16,6 +16,9 @@ import { userFactory } from '@/seeds/factory/user' import { creationFactory } from '@/seeds/factory/creation' import { creations } from '@/seeds/creation/index' import { peterLustig } from '@/seeds/users/peter-lustig' +import { EventProtocol } from '@entity/EventProtocol' +import { EventProtocolType } from '@/event/EventProtocolType' +import { logger } from '@test/testSetup' let mutate: any, query: any, con: any let testEnv: any @@ -35,6 +38,8 @@ afterAll(async () => { }) describe('ContributionResolver', () => { + let bibi: any + describe('createContribution', () => { describe('unauthenticated', () => { it('returns an error', async () => { @@ -54,7 +59,7 @@ describe('ContributionResolver', () => { describe('authenticated with valid user', () => { beforeAll(async () => { await userFactory(testEnv, bibiBloxberg) - await query({ + bibi = await query({ query: login, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, }) @@ -84,6 +89,10 @@ describe('ContributionResolver', () => { ) }) + it('logs the error found', () => { + expect(logger.error).toBeCalledWith(`memo text is too short: memo.length=4 < (5)`) + }) + it('throws error when memo length greater than 255 chars', async () => { const date = new Date() await expect( @@ -102,6 +111,10 @@ describe('ContributionResolver', () => { ) }) + it('logs the error found', () => { + expect(logger.error).toBeCalledWith(`memo text is too long: memo.length=259 > (255)`) + }) + it('throws error when creationDate not-valid', async () => { await expect( mutate({ @@ -121,6 +134,13 @@ describe('ContributionResolver', () => { ) }) + it('logs the error found', () => { + expect(logger.error).toBeCalledWith( + 'No information for available creations with the given creationDate=', + 'Invalid Date', + ) + }) + it('throws error when creationDate 3 month behind', async () => { const date = new Date() await expect( @@ -140,6 +160,13 @@ describe('ContributionResolver', () => { }), ) }) + + it('logs the error found', () => { + expect(logger.error).toBeCalledWith( + 'No information for available creations with the given creationDate=', + 'Invalid Date', + ) + }) }) describe('valid input', () => { @@ -165,6 +192,15 @@ describe('ContributionResolver', () => { }), ) }) + + it('stores the create contribution event in the database', async () => { + await expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.CONTRIBUTION_CREATE, + userId: bibi.data.login.id, + }), + ) + }) }) }) }) @@ -347,6 +383,10 @@ describe('ContributionResolver', () => { }), ) }) + + it('logs the error found', () => { + expect(logger.error).toBeCalledWith('No contribution found to given id') + }) }) describe('Memo length smaller than 5 chars', () => { @@ -368,6 +408,10 @@ describe('ContributionResolver', () => { }), ) }) + + it('logs the error found', () => { + expect(logger.error).toBeCalledWith('memo text is too short: memo.length=4 < (5)') + }) }) describe('Memo length greater than 255 chars', () => { @@ -389,6 +433,10 @@ describe('ContributionResolver', () => { }), ) }) + + it('logs the error found', () => { + expect(logger.error).toBeCalledWith('memo text is too long: memo.length=259 > (255)') + }) }) describe('wrong user tries to update the contribution', () => { @@ -420,6 +468,12 @@ describe('ContributionResolver', () => { }), ) }) + + it('logs the error found', () => { + expect(logger.error).toBeCalledWith( + 'user of the pending contribution and send user does not correspond', + ) + }) }) describe('admin tries to update a user contribution', () => { @@ -441,6 +495,8 @@ describe('ContributionResolver', () => { }), ) }) + + // TODO check that the error is logged (need to modify AdminResolver, avoid conflicts) }) describe('update too much so that the limit is exceeded', () => { @@ -472,6 +528,12 @@ describe('ContributionResolver', () => { }), ) }) + + it('logs the error found', () => { + expect(logger.error).toBeCalledWith( + 'The amount (1019 GDD) to be created exceeds the amount (1000 GDD) still available for this month.', + ) + }) }) describe('update creation to a date that is older than 3 months', () => { @@ -495,6 +557,13 @@ describe('ContributionResolver', () => { }), ) }) + + it('logs the error found', () => { + expect(logger.error).toBeCalledWith( + 'No information for available creations with the given creationDate=', + 'Invalid Date', + ) + }) }) describe('valid input', () => { @@ -521,6 +590,15 @@ describe('ContributionResolver', () => { }), ) }) + + it('stores the update contribution event in the database', async () => { + await expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.CONTRIBUTION_UPDATE, + contributionId: result.data.createContribution.id, + }), + ) + }) }) }) }) @@ -664,9 +742,13 @@ describe('ContributionResolver', () => { }), ) }) + + it('logs the error found', () => { + expect(logger.error).toBeCalledWith('Contribution not found for given id') + }) }) - describe('other user sends a deleteContribtuion', () => { + describe('other user sends a deleteContribution', () => { it('returns an error', async () => { await query({ query: login, @@ -685,6 +767,10 @@ describe('ContributionResolver', () => { }), ) }) + + it('logs the error found', () => { + expect(logger.error).toBeCalledWith('Can not delete contribution of another user') + }) }) describe('User deletes own contribution', () => { @@ -698,6 +784,31 @@ describe('ContributionResolver', () => { }), ).resolves.toBeTruthy() }) + + it('stores the delete contribution event in the database', async () => { + const contribution = await mutate({ + mutation: createContribution, + variables: { + amount: 166.0, + memo: 'Whatever contribution', + creationDate: new Date().toString(), + }, + }) + + await mutate({ + mutation: deleteContribution, + variables: { + id: contribution.data.createContribution.id, + }, + }) + + await expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.CONTRIBUTION_DELETE, + contributionId: contribution.data.createContribution.id, + }), + ) + }) }) describe('User deletes already confirmed contribution', () => { @@ -729,6 +840,10 @@ describe('ContributionResolver', () => { }), ) }) + + it('logs the error found', () => { + expect(logger.error).toBeCalledWith('A confirmed contribution can not be deleted') + }) }) }) }) diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index fc93880f1..e7d9ecdaa 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -13,6 +13,13 @@ import { Contribution, ContributionListResult } from '@model/Contribution' import { UnconfirmedContribution } from '@model/UnconfirmedContribution' import { validateContribution, getUserCreation, updateCreations } from './util/creations' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' +import { + Event, + EventContributionCreate, + EventContributionDelete, + EventContributionUpdate, +} from '@/event/Event' +import { eventProtocol } from '@/event/EventProtocolEmitter' @Resolver() export class ContributionResolver { @@ -23,15 +30,17 @@ export class ContributionResolver { @Ctx() context: Context, ): Promise { if (memo.length > MEMO_MAX_CHARS) { - logger.error(`memo text is too long: memo.length=${memo.length} > (${MEMO_MAX_CHARS}`) + logger.error(`memo text is too long: memo.length=${memo.length} > (${MEMO_MAX_CHARS})`) throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`) } if (memo.length < MEMO_MIN_CHARS) { - logger.error(`memo text is too short: memo.length=${memo.length} < (${MEMO_MIN_CHARS}`) + logger.error(`memo text is too short: memo.length=${memo.length} < (${MEMO_MIN_CHARS})`) throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`) } + const event = new Event() + const user = getUser(context) const creations = await getUserCreation(user.id) logger.trace('creations', creations) @@ -49,6 +58,13 @@ export class ContributionResolver { logger.trace('contribution to save', contribution) await dbContribution.save(contribution) + + const eventCreateContribution = new EventContributionCreate() + eventCreateContribution.userId = user.id + eventCreateContribution.amount = amount + eventCreateContribution.contributionId = contribution.id + await eventProtocol.writeEvent(event.setEventContributionCreate(eventCreateContribution)) + return new UnconfirmedContribution(contribution, user, creations) } @@ -58,19 +74,32 @@ export class ContributionResolver { @Arg('id', () => Int) id: number, @Ctx() context: Context, ): Promise { + const event = new Event() const user = getUser(context) const contribution = await dbContribution.findOne(id) if (!contribution) { + logger.error('Contribution not found for given id') throw new Error('Contribution not found for given id.') } if (contribution.userId !== user.id) { + logger.error('Can not delete contribution of another user') throw new Error('Can not delete contribution of another user') } if (contribution.confirmedAt) { + logger.error('A confirmed contribution can not be deleted') throw new Error('A confirmed contribution can not be deleted') } + contribution.contributionStatus = ContributionStatus.DELETED + contribution.deletedAt = new Date() await contribution.save() + + const eventDeleteContribution = new EventContributionDelete() + eventDeleteContribution.userId = user.id + eventDeleteContribution.contributionId = contribution.id + eventDeleteContribution.amount = contribution.amount + await eventProtocol.writeEvent(event.setEventContributionDelete(eventDeleteContribution)) + const res = await contribution.softRemove() return !!res } @@ -154,9 +183,11 @@ export class ContributionResolver { where: { id: contributionId, confirmedAt: IsNull() }, }) if (!contributionToUpdate) { + logger.error('No contribution found to given id') throw new Error('No contribution found to given id.') } if (contributionToUpdate.userId !== user.id) { + logger.error('user of the pending contribution and send user does not correspond') throw new Error('user of the pending contribution and send user does not correspond') } @@ -174,6 +205,14 @@ export class ContributionResolver { contributionToUpdate.contributionStatus = ContributionStatus.PENDING dbContribution.save(contributionToUpdate) + const event = new Event() + + const eventUpdateContribution = new EventContributionUpdate() + eventUpdateContribution.userId = user.id + eventUpdateContribution.contributionId = contributionId + eventUpdateContribution.amount = amount + await eventProtocol.writeEvent(event.setEventContributionUpdate(eventUpdateContribution)) + return new UnconfirmedContribution(contributionToUpdate, user, creations) } } diff --git a/database/entity/0050-add_messageId_to_event_protocol/EventProtocol.ts b/database/entity/0050-add_messageId_to_event_protocol/EventProtocol.ts new file mode 100644 index 000000000..d4dbc526f --- /dev/null +++ b/database/entity/0050-add_messageId_to_event_protocol/EventProtocol.ts @@ -0,0 +1,42 @@ +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 + + @Column({ name: 'message_id', unsigned: true, nullable: true }) + messageId: number +} diff --git a/database/migrations/0050-add_messageId_to_event_protocol.ts b/database/migrations/0050-add_messageId_to_event_protocol.ts new file mode 100644 index 000000000..ccef98688 --- /dev/null +++ b/database/migrations/0050-add_messageId_to_event_protocol.ts @@ -0,0 +1,12 @@ +/* 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( + `ALTER TABLE \`event_protocol\` ADD COLUMN \`message_id\` int(10) unsigned NULL DEFAULT NULL;`, + ) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn(`ALTER TABLE \`event_protocol\` DROP COLUMN \`message_id\`;`) +} diff --git a/deployment/bare_metal/install.sh b/deployment/bare_metal/install.sh index ddb2706eb..9e60bec08 100755 --- a/deployment/bare_metal/install.sh +++ b/deployment/bare_metal/install.sh @@ -4,6 +4,12 @@ # How to do this is described in detail in [setup.md](./setup.md) # Find current directory & configure paths +## For manualy use in terminal +## set -o allexport +## SCRIPT_DIR=$(pwd) +## PROJECT_ROOT=$SCRIPT_DIR/../.. +## set +o allexport +# Use here in script set -o allexport SCRIPT_PATH=$(realpath $0) SCRIPT_DIR=$(dirname $SCRIPT_PATH) @@ -90,7 +96,7 @@ sudo certbot # Install logrotate sudo apt-get install -y logrotate envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $SCRIPT_DIR/logrotate/gradido.conf.template > $SCRIPT_DIR/logrotate/gradido.conf -sudo mv $SCRIPT_DIR/logrotate/gradido.conf /etc/logrotate.d/gradido.conf +sudo cp $SCRIPT_DIR/logrotate/gradido.conf.template /etc/logrotate.d/gradido.conf sudo chown root:root /etc/logrotate.d/gradido.conf # Install mysql autobackup @@ -137,4 +143,4 @@ envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/admin/.env.te # daily job: 0 4 * * * find /tmp -name "yarn--*" -ctime +1 -exec rm -r {} \; > /dev/null # Start gradido # Note: on first startup some errors will occur - nothing serious -./start.sh \ No newline at end of file +./start.sh diff --git a/deployment/bare_metal/setup.md b/deployment/bare_metal/setup.md index 652a0a5ce..5892cf4fc 100644 --- a/deployment/bare_metal/setup.md +++ b/deployment/bare_metal/setup.md @@ -1,107 +1,233 @@ -# Setup script to setup the server be ready to run gradido -# This assums you have root access via ssh to your cleanly setup server -# Furthermore this assumes you have debian (11 64bit) running -# Check your (Sub-)Domain with your Provider. -# In this document gddhost.tld refers to your chosen domain +# Instructions To Run `Gradido` On Your Server -> ssh root@gddhost.tld +We split setting up `Gradido` on your server into three steps: -# change root default shell -> chsh -s /bin/bash -# Create user `gradido` -> useradd -d /home/gradido -m gradido -> passwd gradido ->> enter new password twice +- [Preparing your server](#command-list-to-setup-your-server-be-ready-to-install-gradido) +- [Installing `Gradido`](#use-commands-in-installsh-manually-in-your-shell-for-now) +- [Crone-Job for `Gradido`](#define-cronjob-to-compensate-yarn-output-in-tmp) -# Gives the user priviledges - this might be omitted in order to harden security -# Care: This will require another administering user if you don't want root access. -# Since this setup expects the user running the software be the same as the administering user, -# you have to adjust the instructions according to that scenario. -# You might lock yourself out, if done wrong. -> usermod -a -G sudo gradido +## Command List To Setup Your Server Be Ready To Install `Gradido` -# change gradido default shell -> chsh -s /bin/bash gradido -# Install sudo -> apt-get install sudo -# switch to the new user -> su gradido +We assume you have root access via ssh to your cleanly setup server. +Furthermore we assume you have debian (11 64bit) running. -# Register first ssh key for user `gradido` -> mkdir ~/.ssh -> chmod 700 ~/.ssh -> nano ~/.ssh/authorized_keys ->> insert public key ->> ctrl + x ->> save +Check your (Sub-)Domain with your Provider. +In this document `gddhost.tld` refers to your chosen domain. -# Test authentication via SSH -> ssh -i /path/to/privKey gradido@gddhost.tld ->> This should log you in and allow you to use sudo commands, which will require the user's password +### SSH into your server -# Disable password authentication & root login -> cd /etc/ssh -> sudo cp sshd_config sshd_config.org -> sudo nano sshd_config ->> change `PermitRootLogin yes` to `PermitRootLogin no` ->> change `#PasswordAuthentication yes` to `PasswordAuthentication no` ->> change `UsePAM yes` to `UsePAM no` ->> ctrl + x ->> save -> sudo /etc/init.d/ssh restart +```bash +ssh root@gddhost.tld +``` -# Test SSH Access only, no root ssh access -> ssh gradido@gddhost.tld ->> Will result in in either a password request for your key or the message `Permission denied (publickey)` -> ssh -i /path/to/privKey root@gddhost.tld ->> Will result in `Permission denied (publickey)` -> ssh -i /path/to/privKey gradido@gddhost.tld ->> Will succeed after entering the correct keys password (if any) +### Change root default shell -# update system -> sudo apt-get update -> sudo apt-get upgrade +```bash +chsh -s /bin/bash +``` -# Install security tools -## ufw -> sudo apt-get install ufw -> sudo ufw allow http -> sudo ufw allow https -> sudo ufw allow ssh -> sudo ufw enable +### Create user `gradido` -## fail2ban -> sudo apt-get install -y fail2ban -> sudo /etc/init.d/fail2ban restart +```bash +$ useradd -d /home/gradido -m gradido +$ passwd gradido +# enter new password twice +``` -# Install gradido -> sudo apt-get install -y git -> cd ~ -> git clone https://github.com/gradido/gradido.git +### Give the user priviledges -# Timezone -# Note: This is needed - since there is Summer-Time included in the default server Setup - UTC is REQUIRED for production data -> sudo timedatectl set-timezone UTC -# > sudo timedatectl set-ntp on -# > sudo apt purge ntp -# > sudo systemctl start systemd-timesyncd -# >> timedatectl to verify +This might be omitted in order to harden security. -# Adjust .env -# NOTE ';' can not be part of any value -# The Github Secret is Created on Github in Settimgs -> Webhooks -> cd gradido/deployment/bare_metal -> cp .env.dist .env -> nano .env ->> Adjust values accordingly -# Define cronjob to compensate yarn output in /tmp -> yarn creates output in /tmp directory, which must be deleted regularly and will be done per cronjob -> on stage1 a hourly job is necessary by setting the following job in the crontab for the gradido user -> crontab -e opens the crontab in edit-mode and insert the following entry: -> "0 * * * * find /tmp -name "yarn--*" -cmin +60 -exec rm -r {} \; > /dev/null" -> on stage2 a daily job is necessary by setting the following job in the crontab for the gradido user -> crontab -e opens the crontab in edit-mode and insert the following entry: -> "0 4 * * * find /tmp -name "yarn--*" -ctime +1 -exec rm -r {} \; > /dev/null" -# TODO the install.sh is not yet ready to run directly - consider to use it as pattern to do it manually -> ./install.sh +***!!! Attention !!!*** + +- Care: This will require another administering user if you don't want root access. +- Since this setup expects the user running the software be the same as the administering user, + - you have to adjust the instructions according to that scenario. + - you might lock yourself out, if done wrong. + +#### Add the new user `gradido` to `sudo` group + +```bash +usermod -a -G sudo gradido +``` + +### Change gradido default shell + +```bash +chsh -s /bin/bash gradido +``` + +### Install sudo + +```bash +apt-get install sudo +``` + +### Switch to the new user + +```bash +su gradido +``` + +### Register first ssh key for user `gradido` + +```bash +$ mkdir ~/.ssh +$ chmod 700 ~/.ssh +$ nano ~/.ssh/authorized_keys +# insert public key +# ctrl + x +# save +``` + +### Test authentication via SSH + +If you logout from the server you can test authentication: + +```bash +$ ssh -i /path/to/privKey gradido@gddhost.tld +# This should log you in and allow you to use sudo commands, which will require the user's password +``` + +### Disable password authentication and root login + +```bash +$ cd /etc/ssh +$ sudo cp sshd_config sshd_config.org +$ sudo nano sshd_config +# change 'PermitRootLogin yes' to `PermitRootLogin no` +# change 'PasswordAuthentication yes' to 'PasswordAuthentication no' +# change 'UsePAM yes' to 'UsePAM no' +# ctrl + x +# save +$ sudo /etc/init.d/ssh restart +``` + +### Test SSH Access only, no root ssh access + +```bash +$ ssh gradido@gddhost.tld +# Will result in in either a passphrase request for your key or the message 'Permission denied (publickey)' +$ ssh -i /path/to/privKey root@gddhost.tld +# Will result in 'Permission denied (publickey)' +$ ssh -i /path/to/privKey gradido@gddhost.tld +# Will succeed after entering the correct keys passphrase (if any) +``` + +### Update system + +```bash +sudo apt-get update +sudo apt-get upgrade +``` + +### Install security tools + +#### Install: `ufw` + +```bash +sudo apt-get install ufw +sudo ufw allow http +sudo ufw allow https +sudo ufw allow ssh +sudo ufw enable +``` + +#### Install: `fail2ban` + +```bash +sudo apt-get install -y fail2ban +sudo /etc/init.d/fail2ban restart +``` + +### Install `Gradido` code + +```bash +sudo apt-get install -y git +cd ~ +git clone https://github.com/gradido/gradido.git +``` + +### Timezone + +*Note: This is needed - since there is Summer-Time included in the default server Setup - UTC is REQUIRED for production data.* + +```bash +sudo timedatectl set-timezone UTC +sudo timedatectl set-ntp on +sudo apt purge ntp +sudo systemctl start systemd-timesyncd +# timedatectl to verify +``` + +### Adjust the values in `.env` + +***!!! Attention !!!*** + +*Don't forget this step! +All your following installations in `install.sh` will fail!* + +*Notes:* + +- *`;` cannot be part of any value!* +- *The GitHub secret is created on GitHub in Settings -> Webhooks.* + +#### Create `.env` and set values + +```bash +$ cd gradido/deployment/bare_metal +$ cp .env.dist .env +$ nano .env +# adjust values accordingly +``` + +## Use Commands In `install.sh` Manually In Your Shell For Now + +The script `install.sh` is not yet ready to run directly. +Use it as pattern to do all steps manually in your terminal shell. + +*TODO: Bring the `install.sh` script to run in the shell.* + +***!!! Attention !!!*** + +- *Commands in `install.sh`:* + - *The commands for setting the paths in the used env variables are not working directly in the terminal, consider the out commented commands for this purpose.* + +Follow the commands in `./install.sh` as installation pattern. + +## Define Cronjob To Compensate Yarn Output In `/tmp` + +`yarn` creates output in `/tmp` directory, which must be deleted regularly and will be done per Cron-Job. + +### On `stage1` + +An hourly job is necessary on `stage1` by setting the following job in the `crontab` for the `gradido` user. + +Run: + +```bash +crontab -e +``` + +This opens the crontab in edit-mode and insert the following entry: + +```bash +0 * * * * find /tmp -name "yarn--*" -cmin +60 -exec rm -r {} \; > /dev/null +``` + +### On `stage2` + +A daily job is necessary on `stage2` by setting the following job in the `crontab` for the `gradido` user. + +Run: + +```bash +crontab -e +``` + +This opens the `crontab` in edit-mode and insert the following entry: + +```bash +0 4 * * * find /tmp -name "yarn--*" -ctime +1 -exec rm -r {} \; > /dev/null +```