mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge master into this branch, resolve merge conflicts
This commit is contained in:
commit
43705b3a40
46
CHANGELOG.md
46
CHANGELOG.md
@ -4,8 +4,54 @@ All notable changes to this project will be documented in this file. Dates are d
|
|||||||
|
|
||||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||||
|
|
||||||
|
#### [1.13.1](https://github.com/gradido/gradido/compare/1.13.0...1.13.1)
|
||||||
|
|
||||||
|
- Fix: correctly evaluate to EMAIL_TEST_MODE to false [`#2273`](https://github.com/gradido/gradido/pull/2273)
|
||||||
|
- Refactor: Contribution resolver logs and events [`#2231`](https://github.com/gradido/gradido/pull/2231)
|
||||||
|
|
||||||
|
#### [1.13.0](https://github.com/gradido/gradido/compare/1.12.1...1.13.0)
|
||||||
|
|
||||||
|
> 18 October 2022
|
||||||
|
|
||||||
|
- release: Version 1.13.0 [`#2269`](https://github.com/gradido/gradido/pull/2269)
|
||||||
|
- fix: Linked User Email in Transaction List [`#2268`](https://github.com/gradido/gradido/pull/2268)
|
||||||
|
- concept capturing alias [`#2148`](https://github.com/gradido/gradido/pull/2148)
|
||||||
|
- fix: 🍰 Daily Redeem Of Contribution Link [`#2265`](https://github.com/gradido/gradido/pull/2265)
|
||||||
|
- fix: 🐛 Prevent Loosing Redeem Code When Changing Between Register and Login in Auth Navbar [`#2260`](https://github.com/gradido/gradido/pull/2260)
|
||||||
|
- fix: Disable Change of Month on Update Contribution [`#2264`](https://github.com/gradido/gradido/pull/2264)
|
||||||
|
- feat: 🍰 Global Jest Extension For Decimal Equal [`#2261`](https://github.com/gradido/gradido/pull/2261)
|
||||||
|
- feat: 🍰 Daily Rule For Contribution Links In Admin Interface [`#2262`](https://github.com/gradido/gradido/pull/2262)
|
||||||
|
- feat: 🍰 Do Not Show Expired Contribution Links In Wallet [`#2257`](https://github.com/gradido/gradido/pull/2257)
|
||||||
|
- fix: 🍰 Disable Change Of Month For Update Contribution (wallet and admin) [`#2258`](https://github.com/gradido/gradido/pull/2258)
|
||||||
|
- refactor: 🍰 Login And Logout To Mutations [`#2232`](https://github.com/gradido/gradido/pull/2232)
|
||||||
|
- fix: 🐛 Verify Token Before Redeeming A Link [`#2254`](https://github.com/gradido/gradido/pull/2254)
|
||||||
|
- Refactor: Add all events to documentation table [`#2240`](https://github.com/gradido/gradido/pull/2240)
|
||||||
|
- reconfig log4js with rollover feature and userid in logevent-message [`#2221`](https://github.com/gradido/gradido/pull/2221)
|
||||||
|
- refactor: 🍰 Refactoring Components Of `CotributionMessagesListItem` [`#2251`](https://github.com/gradido/gradido/pull/2251)
|
||||||
|
- style: add border-radius on send form [`#2233`](https://github.com/gradido/gradido/pull/2233)
|
||||||
|
- 2198 adminarea more dates on created transaction [`#2212`](https://github.com/gradido/gradido/pull/2212)
|
||||||
|
- Bug: delete contribution link [`#2213`](https://github.com/gradido/gradido/pull/2213)
|
||||||
|
- chore: 🍰 Fix Cypress Tests Unreliability [`#2245`](https://github.com/gradido/gradido/pull/2245)
|
||||||
|
- docs: 🍰 Refine Deployment Documentation [`#2209`](https://github.com/gradido/gradido/pull/2209)
|
||||||
|
- End-to-end test setup [`#2047`](https://github.com/gradido/gradido/pull/2047)
|
||||||
|
- config testmodus flag for sending emails to test or team account instead of user account [`#2216`](https://github.com/gradido/gradido/pull/2216)
|
||||||
|
- GradidoID 1: adapt and migrate database schema [`#2058`](https://github.com/gradido/gradido/pull/2058)
|
||||||
|
- feat: Add Client Request Time to Context [`#2206`](https://github.com/gradido/gradido/pull/2206)
|
||||||
|
- 2219 feature rework eventprotocol [`#2234`](https://github.com/gradido/gradido/pull/2234)
|
||||||
|
- Refactor: Test register with redeem code [`#2214`](https://github.com/gradido/gradido/pull/2214)
|
||||||
|
- 2203 delete query modal when redeeming the redeem link [`#2211`](https://github.com/gradido/gradido/pull/2211)
|
||||||
|
- Refactor: 🍰 Change email templates [`#2228`](https://github.com/gradido/gradido/pull/2228)
|
||||||
|
- Refactor: Events and logs completed in User Resolver [`#2204`](https://github.com/gradido/gradido/pull/2204)
|
||||||
|
- change support mail [`#2210`](https://github.com/gradido/gradido/pull/2210)
|
||||||
|
- feat: 🍰 Send email when contribution is confirmed [`#2193`](https://github.com/gradido/gradido/pull/2193)
|
||||||
|
- feat: 🍰 Send email when admin writes message to contribution [`#2187`](https://github.com/gradido/gradido/pull/2187)
|
||||||
|
- feat: 🍰 Send Email To Transaction Link Sender After Receiver Redeemed It [`#2063`](https://github.com/gradido/gradido/pull/2063)
|
||||||
|
|
||||||
#### [1.12.1](https://github.com/gradido/gradido/compare/1.12.0...1.12.1)
|
#### [1.12.1](https://github.com/gradido/gradido/compare/1.12.0...1.12.1)
|
||||||
|
|
||||||
|
> 13 September 2022
|
||||||
|
|
||||||
|
- release: Version 1.12.1 [`#2196`](https://github.com/gradido/gradido/pull/2196)
|
||||||
- fix: 🍰 Show Not Icons In `allContribution` List [`#2195`](https://github.com/gradido/gradido/pull/2195)
|
- fix: 🍰 Show Not Icons In `allContribution` List [`#2195`](https://github.com/gradido/gradido/pull/2195)
|
||||||
|
|
||||||
#### [1.12.0](https://github.com/gradido/gradido/compare/1.11.0...1.12.0)
|
#### [1.12.0](https://github.com/gradido/gradido/compare/1.11.0...1.12.0)
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
"description": "Administraion Interface for Gradido",
|
"description": "Administraion Interface for Gradido",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"author": "Moriz Wahl",
|
"author": "Moriz Wahl",
|
||||||
"version": "1.12.1",
|
"version": "1.13.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gradido-backend",
|
"name": "gradido-backend",
|
||||||
"version": "1.12.1",
|
"version": "1.13.1",
|
||||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": "https://github.com/gradido/gradido/backend",
|
"repository": "https://github.com/gradido/gradido/backend",
|
||||||
|
|||||||
@ -10,7 +10,7 @@ Decimal.set({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const constants = {
|
const constants = {
|
||||||
DB_VERSION: '0051-add_updated_at_to_contributions',
|
DB_VERSION: '0052-add_updated_at_to_contributions',
|
||||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||||
LOG4JS_CONFIG: 'log4js-config.json',
|
LOG4JS_CONFIG: 'log4js-config.json',
|
||||||
// default log level on production should be info
|
// default log level on production should be info
|
||||||
@ -67,7 +67,7 @@ const loginServer = {
|
|||||||
|
|
||||||
const email = {
|
const email = {
|
||||||
EMAIL: process.env.EMAIL === 'true' || false,
|
EMAIL: process.env.EMAIL === 'true' || false,
|
||||||
EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' || 'false',
|
EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' || false,
|
||||||
EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER || 'stage1@gradido.net',
|
EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER || 'stage1@gradido.net',
|
||||||
EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email',
|
EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email',
|
||||||
EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net',
|
EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net',
|
||||||
|
|||||||
@ -11,48 +11,67 @@ export class EventBasicUserId extends EventBasic {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class EventBasicTx extends EventBasicUserId {
|
export class EventBasicTx extends EventBasicUserId {
|
||||||
xUserId: number
|
|
||||||
xCommunityId: number
|
|
||||||
transactionId: number
|
transactionId: number
|
||||||
amount: decimal
|
amount: decimal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class EventBasicTxX extends EventBasicTx {
|
||||||
|
xUserId: number
|
||||||
|
xCommunityId: number
|
||||||
|
}
|
||||||
|
|
||||||
export class EventBasicCt extends EventBasicUserId {
|
export class EventBasicCt extends EventBasicUserId {
|
||||||
contributionId: number
|
contributionId: number
|
||||||
amount: decimal
|
amount: decimal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class EventBasicCtX extends EventBasicCt {
|
||||||
|
xUserId: number
|
||||||
|
xCommunityId: number
|
||||||
|
}
|
||||||
|
|
||||||
export class EventBasicRedeem extends EventBasicUserId {
|
export class EventBasicRedeem extends EventBasicUserId {
|
||||||
transactionId?: number
|
transactionId?: number
|
||||||
contributionId?: number
|
contributionId?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class EventBasicCtMsg extends EventBasicCt {
|
||||||
|
messageId: number
|
||||||
|
}
|
||||||
|
|
||||||
export class EventVisitGradido extends EventBasic {}
|
export class EventVisitGradido extends EventBasic {}
|
||||||
export class EventRegister extends EventBasicUserId {}
|
export class EventRegister extends EventBasicUserId {}
|
||||||
export class EventRedeemRegister extends EventBasicRedeem {}
|
export class EventRedeemRegister extends EventBasicRedeem {}
|
||||||
|
export class EventVerifyRedeem extends EventBasicRedeem {}
|
||||||
export class EventInactiveAccount extends EventBasicUserId {}
|
export class EventInactiveAccount extends EventBasicUserId {}
|
||||||
export class EventSendConfirmationEmail extends EventBasicUserId {}
|
export class EventSendConfirmationEmail extends EventBasicUserId {}
|
||||||
export class EventSendAccountMultiRegistrationEmail 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 EventConfirmationEmail extends EventBasicUserId {}
|
||||||
export class EventRegisterEmailKlicktipp extends EventBasicUserId {}
|
export class EventRegisterEmailKlicktipp extends EventBasicUserId {}
|
||||||
export class EventLogin extends EventBasicUserId {}
|
export class EventLogin extends EventBasicUserId {}
|
||||||
|
export class EventLogout extends EventBasicUserId {}
|
||||||
export class EventRedeemLogin extends EventBasicRedeem {}
|
export class EventRedeemLogin extends EventBasicRedeem {}
|
||||||
export class EventActivateAccount extends EventBasicUserId {}
|
export class EventActivateAccount extends EventBasicUserId {}
|
||||||
export class EventPasswordChange extends EventBasicUserId {}
|
export class EventPasswordChange extends EventBasicUserId {}
|
||||||
export class EventTransactionSend extends EventBasicTx {}
|
export class EventTransactionSend extends EventBasicTxX {}
|
||||||
export class EventTransactionSendRedeem extends EventBasicTx {}
|
export class EventTransactionSendRedeem extends EventBasicTxX {}
|
||||||
export class EventTransactionRepeateRedeem extends EventBasicTx {}
|
export class EventTransactionRepeateRedeem extends EventBasicTxX {}
|
||||||
export class EventTransactionCreation extends EventBasicUserId {
|
export class EventTransactionCreation extends EventBasicTx {}
|
||||||
transactionId: number
|
export class EventTransactionReceive extends EventBasicTxX {}
|
||||||
amount: decimal
|
export class EventTransactionReceiveRedeem extends EventBasicTxX {}
|
||||||
}
|
|
||||||
export class EventTransactionReceive extends EventBasicTx {}
|
|
||||||
export class EventTransactionReceiveRedeem extends EventBasicTx {}
|
|
||||||
export class EventContributionCreate extends EventBasicCt {}
|
export class EventContributionCreate extends EventBasicCt {}
|
||||||
export class EventContributionConfirm extends EventBasicCt {
|
export class EventUserCreateContributionMessage extends EventBasicCtMsg {}
|
||||||
xUserId: number
|
export class EventAdminCreateContributionMessage extends EventBasicCtMsg {}
|
||||||
xCommunityId: number
|
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 EventContributionLinkDefine extends EventBasicCt {}
|
||||||
export class EventContributionLinkActivateRedeem extends EventBasicCt {}
|
export class EventContributionLinkActivateRedeem extends EventBasicCt {}
|
||||||
|
|
||||||
@ -100,6 +119,13 @@ export class Event {
|
|||||||
return this
|
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 {
|
public setEventInactiveAccount(ev: EventInactiveAccount): Event {
|
||||||
this.setByBasicUser(ev.userId)
|
this.setByBasicUser(ev.userId)
|
||||||
this.type = EventProtocolType.INACTIVE_ACCOUNT
|
this.type = EventProtocolType.INACTIVE_ACCOUNT
|
||||||
@ -118,7 +144,49 @@ export class Event {
|
|||||||
ev: EventSendAccountMultiRegistrationEmail,
|
ev: EventSendAccountMultiRegistrationEmail,
|
||||||
): Event {
|
): Event {
|
||||||
this.setByBasicUser(ev.userId)
|
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
|
return this
|
||||||
}
|
}
|
||||||
@ -144,6 +212,13 @@ export class Event {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setEventLogout(ev: EventLogout): Event {
|
||||||
|
this.setByBasicUser(ev.userId)
|
||||||
|
this.type = EventProtocolType.LOGOUT
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
public setEventRedeemLogin(ev: EventRedeemLogin): Event {
|
public setEventRedeemLogin(ev: EventRedeemLogin): Event {
|
||||||
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId)
|
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId)
|
||||||
this.type = EventProtocolType.REDEEM_LOGIN
|
this.type = EventProtocolType.REDEEM_LOGIN
|
||||||
@ -166,44 +241,42 @@ export class Event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setEventTransactionSend(ev: EventTransactionSend): 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
|
this.type = EventProtocolType.TRANSACTION_SEND
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
public setEventTransactionSendRedeem(ev: EventTransactionSendRedeem): Event {
|
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
|
this.type = EventProtocolType.TRANSACTION_SEND_REDEEM
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
public setEventTransactionRepeateRedeem(ev: EventTransactionRepeateRedeem): Event {
|
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
|
this.type = EventProtocolType.TRANSACTION_REPEATE_REDEEM
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
public setEventTransactionCreation(ev: EventTransactionCreation): Event {
|
public setEventTransactionCreation(ev: EventTransactionCreation): Event {
|
||||||
this.setByBasicUser(ev.userId)
|
this.setByBasicTx(ev.userId, ev.transactionId, ev.amount)
|
||||||
if (ev.transactionId) this.transactionId = ev.transactionId
|
|
||||||
if (ev.amount) this.amount = ev.amount
|
|
||||||
this.type = EventProtocolType.TRANSACTION_CREATION
|
this.type = EventProtocolType.TRANSACTION_CREATION
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
public setEventTransactionReceive(ev: EventTransactionReceive): Event {
|
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
|
this.type = EventProtocolType.TRANSACTION_RECEIVE
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
public setEventTransactionReceiveRedeem(ev: EventTransactionReceiveRedeem): Event {
|
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
|
this.type = EventProtocolType.TRANSACTION_RECEIVE_REDEEM
|
||||||
|
|
||||||
return this
|
return this
|
||||||
@ -216,15 +289,48 @@ export class Event {
|
|||||||
return this
|
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)
|
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||||
if (ev.xUserId) this.xUserId = ev.xUserId
|
this.type = EventProtocolType.CONTRIBUTION_DELETE
|
||||||
if (ev.xCommunityId) this.xCommunityId = ev.xCommunityId
|
|
||||||
|
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.contributionId, ev.amount, ev.xUserId, ev.xCommunityId)
|
||||||
this.type = EventProtocolType.CONTRIBUTION_CONFIRM
|
this.type = EventProtocolType.CONTRIBUTION_CONFIRM
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setEventContributionDeny(ev: EventContributionDeny): Event {
|
||||||
|
this.setByBasicCtX(ev.userId, ev.contributionId, ev.amount, ev.xUserId, ev.xCommunityId)
|
||||||
|
this.type = EventProtocolType.CONTRIBUTION_DENY
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
public setEventContributionLinkDefine(ev: EventContributionLinkDefine): Event {
|
public setEventContributionLinkDefine(ev: EventContributionLinkDefine): Event {
|
||||||
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||||
this.type = EventProtocolType.CONTRIBUTION_LINK_DEFINE
|
this.type = EventProtocolType.CONTRIBUTION_LINK_DEFINE
|
||||||
@ -246,26 +352,58 @@ export class Event {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
setByBasicTx(
|
setByBasicTx(userId: number, transactionId: number, amount: decimal): Event {
|
||||||
userId: number,
|
|
||||||
xUserId?: number,
|
|
||||||
xCommunityId?: number,
|
|
||||||
transactionId?: number,
|
|
||||||
amount?: decimal,
|
|
||||||
): Event {
|
|
||||||
this.setByBasicUser(userId)
|
this.setByBasicUser(userId)
|
||||||
if (xUserId) this.xUserId = xUserId
|
this.transactionId = transactionId
|
||||||
if (xCommunityId) this.xCommunityId = xCommunityId
|
this.amount = amount
|
||||||
if (transactionId) this.transactionId = transactionId
|
|
||||||
if (amount) this.amount = amount
|
|
||||||
|
|
||||||
return this
|
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)
|
this.setByBasicUser(userId)
|
||||||
if (contributionId) this.contributionId = contributionId
|
this.contributionId = contributionId
|
||||||
if (amount) this.amount = amount
|
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
|
return this
|
||||||
}
|
}
|
||||||
@ -278,27 +416,6 @@ export class Event {
|
|||||||
return this
|
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
|
id: number
|
||||||
type: string
|
type: string
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
@ -308,4 +425,5 @@ export class Event {
|
|||||||
transactionId?: number
|
transactionId?: number
|
||||||
contributionId?: number
|
contributionId?: number
|
||||||
amount?: decimal
|
amount?: decimal
|
||||||
|
messageId?: number
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,23 +3,36 @@ export enum EventProtocolType {
|
|||||||
VISIT_GRADIDO = 'VISIT_GRADIDO',
|
VISIT_GRADIDO = 'VISIT_GRADIDO',
|
||||||
REGISTER = 'REGISTER',
|
REGISTER = 'REGISTER',
|
||||||
REDEEM_REGISTER = 'REDEEM_REGISTER',
|
REDEEM_REGISTER = 'REDEEM_REGISTER',
|
||||||
|
VERIFY_REDEEM = 'VERIFY_REDEEM',
|
||||||
INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
|
INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
|
||||||
SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL',
|
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',
|
CONFIRM_EMAIL = 'CONFIRM_EMAIL',
|
||||||
REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP',
|
REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP',
|
||||||
LOGIN = 'LOGIN',
|
LOGIN = 'LOGIN',
|
||||||
|
LOGOUT = 'LOGOUT',
|
||||||
REDEEM_LOGIN = 'REDEEM_LOGIN',
|
REDEEM_LOGIN = 'REDEEM_LOGIN',
|
||||||
ACTIVATE_ACCOUNT = 'ACTIVATE_ACCOUNT',
|
ACTIVATE_ACCOUNT = 'ACTIVATE_ACCOUNT',
|
||||||
|
SEND_FORGOT_PASSWORD_EMAIL = 'SEND_FORGOT_PASSWORD_EMAIL',
|
||||||
PASSWORD_CHANGE = 'PASSWORD_CHANGE',
|
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 = 'TRANSACTION_SEND',
|
||||||
TRANSACTION_SEND_REDEEM = 'TRANSACTION_SEND_REDEEM',
|
TRANSACTION_SEND_REDEEM = 'TRANSACTION_SEND_REDEEM',
|
||||||
TRANSACTION_REPEATE_REDEEM = 'TRANSACTION_REPEATE_REDEEM',
|
TRANSACTION_REPEATE_REDEEM = 'TRANSACTION_REPEATE_REDEEM',
|
||||||
TRANSACTION_CREATION = 'TRANSACTION_CREATION',
|
TRANSACTION_CREATION = 'TRANSACTION_CREATION',
|
||||||
TRANSACTION_RECEIVE = 'TRANSACTION_RECEIVE',
|
TRANSACTION_RECEIVE = 'TRANSACTION_RECEIVE',
|
||||||
TRANSACTION_RECEIVE_REDEEM = 'TRANSACTION_RECEIVE_REDEEM',
|
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_CREATE = 'CONTRIBUTION_CREATE',
|
||||||
CONTRIBUTION_CONFIRM = 'CONTRIBUTION_CONFIRM',
|
CONTRIBUTION_CONFIRM = 'CONTRIBUTION_CONFIRM',
|
||||||
|
CONTRIBUTION_DENY = 'CONTRIBUTION_DENY',
|
||||||
CONTRIBUTION_LINK_DEFINE = 'CONTRIBUTION_LINK_DEFINE',
|
CONTRIBUTION_LINK_DEFINE = 'CONTRIBUTION_LINK_DEFINE',
|
||||||
CONTRIBUTION_LINK_ACTIVATE_REDEEM = 'CONTRIBUTION_LINK_ACTIVATE_REDEEM',
|
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',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import {
|
|||||||
setUserRole,
|
setUserRole,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
unDeleteUser,
|
unDeleteUser,
|
||||||
|
createContribution,
|
||||||
adminCreateContribution,
|
adminCreateContribution,
|
||||||
adminCreateContributions,
|
adminCreateContributions,
|
||||||
adminUpdateContribution,
|
adminUpdateContribution,
|
||||||
@ -77,6 +78,7 @@ afterAll(async () => {
|
|||||||
let admin: User
|
let admin: User
|
||||||
let user: User
|
let user: User
|
||||||
let creation: Contribution | void
|
let creation: Contribution | void
|
||||||
|
let result: any
|
||||||
|
|
||||||
describe('AdminResolver', () => {
|
describe('AdminResolver', () => {
|
||||||
describe('set user role', () => {
|
describe('set user role', () => {
|
||||||
@ -1365,6 +1367,38 @@ describe('AdminResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('admin deletes own user contribution', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await query({
|
||||||
|
query: login,
|
||||||
|
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
result = await mutate({
|
||||||
|
mutation: createContribution,
|
||||||
|
variables: {
|
||||||
|
amount: 100.0,
|
||||||
|
memo: 'Test env contribution',
|
||||||
|
creationDate: new Date().toString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an error', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: adminDeleteContribution,
|
||||||
|
variables: {
|
||||||
|
id: result.data.createContribution.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [new GraphQLError('Own contribution can not be deleted as admin')],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('creation id does exist', () => {
|
describe('creation id does exist', () => {
|
||||||
it('returns true', async () => {
|
it('returns true', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
|
|||||||
@ -400,13 +400,24 @@ export class AdminResolver {
|
|||||||
|
|
||||||
@Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION])
|
@Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION])
|
||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
async adminDeleteContribution(@Arg('id', () => Int) id: number): Promise<boolean> {
|
async adminDeleteContribution(
|
||||||
|
@Arg('id', () => Int) id: number,
|
||||||
|
@Ctx() context: Context,
|
||||||
|
): Promise<boolean> {
|
||||||
const contribution = await DbContribution.findOne(id)
|
const contribution = await DbContribution.findOne(id)
|
||||||
if (!contribution) {
|
if (!contribution) {
|
||||||
logger.error(`Contribution not found for given id: ${id}`)
|
logger.error(`Contribution not found for given id: ${id}`)
|
||||||
throw new Error('Contribution not found for given id.')
|
throw new Error('Contribution not found for given id.')
|
||||||
}
|
}
|
||||||
|
const moderator = getUser(context)
|
||||||
|
if (
|
||||||
|
contribution.contributionType === ContributionType.USER &&
|
||||||
|
contribution.userId === moderator.id
|
||||||
|
) {
|
||||||
|
throw new Error('Own contribution can not be deleted as admin')
|
||||||
|
}
|
||||||
contribution.contributionStatus = ContributionStatus.DELETED
|
contribution.contributionStatus = ContributionStatus.DELETED
|
||||||
|
contribution.deletedBy = moderator.id
|
||||||
await contribution.save()
|
await contribution.save()
|
||||||
const res = await contribution.softRemove()
|
const res = await contribution.softRemove()
|
||||||
return !!res
|
return !!res
|
||||||
|
|||||||
@ -17,6 +17,9 @@ import { userFactory } from '@/seeds/factory/user'
|
|||||||
import { creationFactory } from '@/seeds/factory/creation'
|
import { creationFactory } from '@/seeds/factory/creation'
|
||||||
import { creations } from '@/seeds/creation/index'
|
import { creations } from '@/seeds/creation/index'
|
||||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
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 mutate: any, query: any, con: any
|
||||||
let testEnv: any
|
let testEnv: any
|
||||||
@ -36,6 +39,8 @@ afterAll(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('ContributionResolver', () => {
|
describe('ContributionResolver', () => {
|
||||||
|
let bibi: any
|
||||||
|
|
||||||
describe('createContribution', () => {
|
describe('createContribution', () => {
|
||||||
describe('unauthenticated', () => {
|
describe('unauthenticated', () => {
|
||||||
it('returns an error', async () => {
|
it('returns an error', async () => {
|
||||||
@ -55,7 +60,8 @@ describe('ContributionResolver', () => {
|
|||||||
describe('authenticated with valid user', () => {
|
describe('authenticated with valid user', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await userFactory(testEnv, bibiBloxberg)
|
await userFactory(testEnv, bibiBloxberg)
|
||||||
await mutate({
|
|
||||||
|
bibi = await mutate({
|
||||||
mutation: login,
|
mutation: login,
|
||||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||||
})
|
})
|
||||||
@ -85,6 +91,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 () => {
|
it('throws error when memo length greater than 255 chars', async () => {
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
await expect(
|
await expect(
|
||||||
@ -103,6 +113,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 () => {
|
it('throws error when creationDate not-valid', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
@ -122,6 +136,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 () => {
|
it('throws error when creationDate 3 month behind', async () => {
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
await expect(
|
await expect(
|
||||||
@ -141,20 +162,31 @@ 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', () => {
|
describe('valid input', () => {
|
||||||
|
let contribution: any
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
contribution = await mutate({
|
||||||
|
mutation: createContribution,
|
||||||
|
variables: {
|
||||||
|
amount: 100.0,
|
||||||
|
memo: 'Test env contribution',
|
||||||
|
creationDate: new Date().toString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('creates contribution', async () => {
|
it('creates contribution', async () => {
|
||||||
await expect(
|
expect(contribution).toEqual(
|
||||||
mutate({
|
|
||||||
mutation: createContribution,
|
|
||||||
variables: {
|
|
||||||
amount: 100.0,
|
|
||||||
memo: 'Test env contribution',
|
|
||||||
creationDate: new Date().toString(),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).resolves.toEqual(
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
data: {
|
data: {
|
||||||
createContribution: {
|
createContribution: {
|
||||||
@ -166,6 +198,17 @@ describe('ContributionResolver', () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('stores the create contribution event in the database', async () => {
|
||||||
|
await expect(EventProtocol.find()).resolves.toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: EventProtocolType.CONTRIBUTION_CREATE,
|
||||||
|
amount: expect.decimalEqual(100),
|
||||||
|
contributionId: contribution.data.createContribution.id,
|
||||||
|
userId: bibi.data.login.id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -348,6 +391,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', () => {
|
describe('Memo length smaller than 5 chars', () => {
|
||||||
@ -369,6 +416,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', () => {
|
describe('Memo length greater than 255 chars', () => {
|
||||||
@ -390,6 +441,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', () => {
|
describe('wrong user tries to update the contribution', () => {
|
||||||
@ -421,6 +476,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', () => {
|
describe('admin tries to update a user contribution', () => {
|
||||||
@ -442,6 +503,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', () => {
|
describe('update too much so that the limit is exceeded', () => {
|
||||||
@ -473,6 +536,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', () => {
|
describe('update creation to a date that is older than 3 months', () => {
|
||||||
@ -494,6 +563,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', () => {
|
describe('valid input', () => {
|
||||||
@ -520,6 +596,22 @@ describe('ContributionResolver', () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('stores the update contribution event in the database', async () => {
|
||||||
|
bibi = await query({
|
||||||
|
query: login,
|
||||||
|
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(EventProtocol.find()).resolves.toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: EventProtocolType.CONTRIBUTION_UPDATE,
|
||||||
|
amount: expect.decimalEqual(10),
|
||||||
|
contributionId: result.data.createContribution.id,
|
||||||
|
userId: bibi.data.login.id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -626,9 +718,11 @@ describe('ContributionResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('authenticated', () => {
|
describe('authenticated', () => {
|
||||||
|
let peter: any
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await userFactory(testEnv, bibiBloxberg)
|
await userFactory(testEnv, bibiBloxberg)
|
||||||
await userFactory(testEnv, peterLustig)
|
peter = await userFactory(testEnv, peterLustig)
|
||||||
|
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: login,
|
mutation: login,
|
||||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||||
@ -663,9 +757,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 () => {
|
it('returns an error', async () => {
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: login,
|
mutation: login,
|
||||||
@ -684,6 +782,10 @@ describe('ContributionResolver', () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('logs the error found', () => {
|
||||||
|
expect(logger.error).toBeCalledWith('Can not delete contribution of another user')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('User deletes own contribution', () => {
|
describe('User deletes own contribution', () => {
|
||||||
@ -697,6 +799,33 @@ describe('ContributionResolver', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toBeTruthy()
|
).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,
|
||||||
|
amount: expect.decimalEqual(166),
|
||||||
|
userId: peter.id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('User deletes already confirmed contribution', () => {
|
describe('User deletes already confirmed contribution', () => {
|
||||||
@ -728,6 +857,10 @@ describe('ContributionResolver', () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('logs the error found', () => {
|
||||||
|
expect(logger.error).toBeCalledWith('A confirmed contribution can not be deleted')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -15,6 +15,13 @@ import { validateContribution, getUserCreation, updateCreations } from './util/c
|
|||||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||||
import { ContributionMessage } from '@entity/ContributionMessage'
|
import { ContributionMessage } from '@entity/ContributionMessage'
|
||||||
import { ContributionMessageType } from '../enum/MessageType'
|
import { ContributionMessageType } from '../enum/MessageType'
|
||||||
|
import {
|
||||||
|
Event,
|
||||||
|
EventContributionCreate,
|
||||||
|
EventContributionDelete,
|
||||||
|
EventContributionUpdate,
|
||||||
|
} from '@/event/Event'
|
||||||
|
import { eventProtocol } from '@/event/EventProtocolEmitter'
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class ContributionResolver {
|
export class ContributionResolver {
|
||||||
@ -25,15 +32,17 @@ export class ContributionResolver {
|
|||||||
@Ctx() context: Context,
|
@Ctx() context: Context,
|
||||||
): Promise<UnconfirmedContribution> {
|
): Promise<UnconfirmedContribution> {
|
||||||
if (memo.length > MEMO_MAX_CHARS) {
|
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)`)
|
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (memo.length < MEMO_MIN_CHARS) {
|
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)`)
|
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const event = new Event()
|
||||||
|
|
||||||
const user = getUser(context)
|
const user = getUser(context)
|
||||||
const creations = await getUserCreation(user.id)
|
const creations = await getUserCreation(user.id)
|
||||||
logger.trace('creations', creations)
|
logger.trace('creations', creations)
|
||||||
@ -51,6 +60,13 @@ export class ContributionResolver {
|
|||||||
|
|
||||||
logger.trace('contribution to save', contribution)
|
logger.trace('contribution to save', contribution)
|
||||||
await dbContribution.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)
|
return new UnconfirmedContribution(contribution, user, creations)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,19 +76,33 @@ export class ContributionResolver {
|
|||||||
@Arg('id', () => Int) id: number,
|
@Arg('id', () => Int) id: number,
|
||||||
@Ctx() context: Context,
|
@Ctx() context: Context,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
const event = new Event()
|
||||||
const user = getUser(context)
|
const user = getUser(context)
|
||||||
const contribution = await dbContribution.findOne(id)
|
const contribution = await dbContribution.findOne(id)
|
||||||
if (!contribution) {
|
if (!contribution) {
|
||||||
|
logger.error('Contribution not found for given id')
|
||||||
throw new Error('Contribution not found for given id.')
|
throw new Error('Contribution not found for given id.')
|
||||||
}
|
}
|
||||||
if (contribution.userId !== user.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')
|
throw new Error('Can not delete contribution of another user')
|
||||||
}
|
}
|
||||||
if (contribution.confirmedAt) {
|
if (contribution.confirmedAt) {
|
||||||
|
logger.error('A confirmed contribution can not be deleted')
|
||||||
throw new Error('A confirmed contribution can not be deleted')
|
throw new Error('A confirmed contribution can not be deleted')
|
||||||
}
|
}
|
||||||
|
|
||||||
contribution.contributionStatus = ContributionStatus.DELETED
|
contribution.contributionStatus = ContributionStatus.DELETED
|
||||||
|
contribution.deletedBy = user.id
|
||||||
|
contribution.deletedAt = new Date()
|
||||||
await contribution.save()
|
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()
|
const res = await contribution.softRemove()
|
||||||
return !!res
|
return !!res
|
||||||
}
|
}
|
||||||
@ -100,6 +130,7 @@ export class ContributionResolver {
|
|||||||
.from(dbContribution, 'c')
|
.from(dbContribution, 'c')
|
||||||
.leftJoinAndSelect('c.messages', 'm')
|
.leftJoinAndSelect('c.messages', 'm')
|
||||||
.where(where)
|
.where(where)
|
||||||
|
.withDeleted()
|
||||||
.orderBy('c.createdAt', order)
|
.orderBy('c.createdAt', order)
|
||||||
.limit(pageSize)
|
.limit(pageSize)
|
||||||
.offset((currentPage - 1) * pageSize)
|
.offset((currentPage - 1) * pageSize)
|
||||||
@ -156,9 +187,11 @@ export class ContributionResolver {
|
|||||||
where: { id: contributionId, confirmedAt: IsNull() },
|
where: { id: contributionId, confirmedAt: IsNull() },
|
||||||
})
|
})
|
||||||
if (!contributionToUpdate) {
|
if (!contributionToUpdate) {
|
||||||
|
logger.error('No contribution found to given id')
|
||||||
throw new Error('No contribution found to given id.')
|
throw new Error('No contribution found to given id.')
|
||||||
}
|
}
|
||||||
if (contributionToUpdate.userId !== user.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')
|
throw new Error('user of the pending contribution and send user does not correspond')
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@ -186,6 +219,17 @@ export class ContributionResolver {
|
|||||||
contributionMessage.createdAt = contributionToUpdate.updatedAt
|
contributionMessage.createdAt = contributionToUpdate.updatedAt
|
||||||
? contributionToUpdate.updatedAt
|
? contributionToUpdate.updatedAt
|
||||||
: contributionToUpdate.createdAt
|
: contributionToUpdate.createdAt
|
||||||
|
const newMessage = ''
|
||||||
|
if (contributionToUpdate.memo !== memo) {
|
||||||
|
//
|
||||||
|
|
||||||
|
}
|
||||||
|
if (contributionToUpdate.amount !== amount) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
if (contributionToUpdate.contributionDate !== new Date(creationDate)) {
|
||||||
|
//
|
||||||
|
}
|
||||||
contributionMessage.message = ``
|
contributionMessage.message = ``
|
||||||
contributionMessage.type = ContributionMessageType.HISTORY
|
contributionMessage.type = ContributionMessageType.HISTORY
|
||||||
|
|
||||||
@ -196,6 +240,14 @@ export class ContributionResolver {
|
|||||||
contributionToUpdate.updatedAt = new Date()
|
contributionToUpdate.updatedAt = new Date()
|
||||||
dbContribution.save(contributionToUpdate)
|
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)
|
return new UnconfirmedContribution(contributionToUpdate, user, creations)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,8 +73,8 @@ describe('TransactionLinkResolver', () => {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
code: expect.stringMatching(/^[0-9a-f]{24,24}$/),
|
code: expect.stringMatching(/^[0-9a-f]{24,24}$/),
|
||||||
linkEnabled: true,
|
linkEnabled: true,
|
||||||
// amount: '200',
|
amount: expect.decimalEqual(5),
|
||||||
// maxAmountPerMonth: '200',
|
maxAmountPerMonth: expect.decimalEqual(200),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -111,6 +111,56 @@ describe('TransactionLinkResolver', () => {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('after one day', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.useFakeTimers()
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
|
||||||
|
setTimeout(() => {}, 1000 * 60 * 60 * 24)
|
||||||
|
jest.runAllTimers()
|
||||||
|
await mutate({
|
||||||
|
mutation: login,
|
||||||
|
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows the user to redeem the contribution link again', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: redeemTransactionLink,
|
||||||
|
variables: {
|
||||||
|
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
redeemTransactionLink: true,
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: redeemTransactionLink,
|
||||||
|
variables: {
|
||||||
|
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
errors: [
|
||||||
|
new GraphQLError(
|
||||||
|
'Creation from contribution link was not successful. Error: Contribution link already redeemed today',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { backendLogger as logger } from '@/server/logger'
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
import { Context, getUser } from '@/server/context'
|
import { Context, getUser } from '@/server/context'
|
||||||
import { getConnection, Between } from '@dbTools/typeorm'
|
import { getConnection } from '@dbTools/typeorm'
|
||||||
import {
|
import {
|
||||||
Resolver,
|
Resolver,
|
||||||
Args,
|
Args,
|
||||||
@ -235,11 +235,16 @@ export class TransactionLinkResolver {
|
|||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.select('contribution')
|
.select('contribution')
|
||||||
.from(DbContribution, 'contribution')
|
.from(DbContribution, 'contribution')
|
||||||
.where('contribution.contributionLinkId = :linkId AND contribution.userId = :id', {
|
.where(
|
||||||
linkId: contributionLink.id,
|
`contribution.contributionLinkId = :linkId AND contribution.userId = :id
|
||||||
id: user.id,
|
AND Date(contribution.confirmedAt) BETWEEN :start AND :end`,
|
||||||
contributionDate: Between(start, end),
|
{
|
||||||
})
|
linkId: contributionLink.id,
|
||||||
|
id: user.id,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
)
|
||||||
.getOne()
|
.getOne()
|
||||||
if (alreadyRedeemed) {
|
if (alreadyRedeemed) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import CONFIG from '@/config'
|
|||||||
|
|
||||||
import { Context, getUser } from '@/server/context'
|
import { Context, getUser } from '@/server/context'
|
||||||
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
|
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
|
||||||
import { getCustomRepository, getConnection } from '@dbTools/typeorm'
|
import { getCustomRepository, getConnection, In } from '@dbTools/typeorm'
|
||||||
|
|
||||||
import { sendTransactionReceivedEmail } from '@/mailer/sendTransactionReceivedEmail'
|
import { sendTransactionReceivedEmail } from '@/mailer/sendTransactionReceivedEmail'
|
||||||
|
|
||||||
@ -224,11 +224,11 @@ export class TransactionResolver {
|
|||||||
logger.debug(`involvedUserIds=${involvedUserIds}`)
|
logger.debug(`involvedUserIds=${involvedUserIds}`)
|
||||||
|
|
||||||
// We need to show the name for deleted users for old transactions
|
// We need to show the name for deleted users for old transactions
|
||||||
const involvedDbUsers = await dbUser
|
const involvedDbUsers = await dbUser.find({
|
||||||
.createQueryBuilder()
|
where: { id: In(involvedUserIds) },
|
||||||
.withDeleted()
|
withDeleted: true,
|
||||||
.where('id IN (:...userIds)', { userIds: involvedUserIds })
|
relations: ['emailContact'],
|
||||||
.getMany()
|
})
|
||||||
const involvedUsers = involvedDbUsers.map((u) => new User(u))
|
const involvedUsers = involvedDbUsers.map((u) => new User(u))
|
||||||
logger.debug(`involvedUsers=${involvedUsers}`)
|
logger.debug(`involvedUsers=${involvedUsers}`)
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export const validateContribution = (
|
|||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
logger.error(
|
logger.error(
|
||||||
'No information for available creations with the given creationDate=',
|
'No information for available creations with the given creationDate=',
|
||||||
creationDate,
|
creationDate.toString(),
|
||||||
)
|
)
|
||||||
throw new Error('No information for available creations for the given date')
|
throw new Error('No information for available creations for the given date')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ describe('sendAddedContributionMessageEmail', () => {
|
|||||||
it('calls sendEMail', () => {
|
it('calls sendEMail', () => {
|
||||||
expect(sendEMail).toBeCalledWith({
|
expect(sendEMail).toBeCalledWith({
|
||||||
to: `Bibi Bloxberg <bibi@bloxberg.de>`,
|
to: `Bibi Bloxberg <bibi@bloxberg.de>`,
|
||||||
subject: 'Gradido Frage zur Schöpfung',
|
subject: 'Rückfrage zu Deinem Gemeinwohl-Beitrag',
|
||||||
text:
|
text:
|
||||||
expect.stringContaining('Hallo Bibi Bloxberg') &&
|
expect.stringContaining('Hallo Bibi Bloxberg') &&
|
||||||
expect.stringContaining('Peter Lustig') &&
|
expect.stringContaining('Peter Lustig') &&
|
||||||
|
|||||||
@ -29,6 +29,7 @@ describe('sendEMail', () => {
|
|||||||
let result: boolean
|
let result: boolean
|
||||||
describe('config email is false', () => {
|
describe('config email is false', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
result = await sendEMail({
|
result = await sendEMail({
|
||||||
to: 'receiver@mail.org',
|
to: 'receiver@mail.org',
|
||||||
cc: 'support@gradido.net',
|
cc: 'support@gradido.net',
|
||||||
@ -48,6 +49,7 @@ describe('sendEMail', () => {
|
|||||||
|
|
||||||
describe('config email is true', () => {
|
describe('config email is true', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
CONFIG.EMAIL = true
|
CONFIG.EMAIL = true
|
||||||
result = await sendEMail({
|
result = await sendEMail({
|
||||||
to: 'receiver@mail.org',
|
to: 'receiver@mail.org',
|
||||||
@ -73,7 +75,7 @@ describe('sendEMail', () => {
|
|||||||
it('calls sendMail of transporter', () => {
|
it('calls sendMail of transporter', () => {
|
||||||
expect((createTransport as jest.Mock).mock.results[0].value.sendMail).toBeCalledWith({
|
expect((createTransport as jest.Mock).mock.results[0].value.sendMail).toBeCalledWith({
|
||||||
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
||||||
to: `${CONFIG.EMAIL_TEST_RECEIVER}`,
|
to: 'receiver@mail.org',
|
||||||
cc: 'support@gradido.net',
|
cc: 'support@gradido.net',
|
||||||
subject: 'Subject',
|
subject: 'Subject',
|
||||||
text: 'Text text text',
|
text: 'Text text text',
|
||||||
@ -84,4 +86,28 @@ describe('sendEMail', () => {
|
|||||||
expect(result).toBeTruthy()
|
expect(result).toBeTruthy()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('with email EMAIL_TEST_MODUS true', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
CONFIG.EMAIL = true
|
||||||
|
CONFIG.EMAIL_TEST_MODUS = true
|
||||||
|
result = await sendEMail({
|
||||||
|
to: 'receiver@mail.org',
|
||||||
|
cc: 'support@gradido.net',
|
||||||
|
subject: 'Subject',
|
||||||
|
text: 'Text text text',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls sendMail of transporter with faked to', () => {
|
||||||
|
expect((createTransport as jest.Mock).mock.results[0].value.sendMail).toBeCalledWith({
|
||||||
|
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
||||||
|
to: CONFIG.EMAIL_TEST_RECEIVER,
|
||||||
|
cc: 'support@gradido.net',
|
||||||
|
subject: 'Subject',
|
||||||
|
text: 'Text text text',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
export const contributionMessageReceived = {
|
export const contributionMessageReceived = {
|
||||||
de: {
|
de: {
|
||||||
subject: 'Gradido Frage zur Schöpfung',
|
subject: 'Rückfrage zu Deinem Gemeinwohl-Beitrag',
|
||||||
text: (data: {
|
text: (data: {
|
||||||
senderFirstName: string
|
senderFirstName: string
|
||||||
senderLastName: string
|
senderLastName: string
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
@ -83,10 +83,10 @@ export class Contribution extends BaseEntity {
|
|||||||
@DeleteDateColumn({ name: 'deleted_at' })
|
@DeleteDateColumn({ name: 'deleted_at' })
|
||||||
deletedAt: Date | null
|
deletedAt: Date | null
|
||||||
|
|
||||||
|
@DeleteDateColumn({ unsigned: true, nullable: true, name: 'deleted_by' })
|
||||||
|
deletedBy: number
|
||||||
|
|
||||||
@OneToMany(() => ContributionMessage, (message) => message.contribution)
|
@OneToMany(() => ContributionMessage, (message) => message.contribution)
|
||||||
@JoinColumn({ name: 'contribution_id' })
|
@JoinColumn({ name: 'contribution_id' })
|
||||||
messages?: ContributionMessage[]
|
messages?: ContributionMessage[]
|
||||||
|
|
||||||
@Column({ nullable: true, name: 'updated_at' })
|
|
||||||
updatedAt: Date
|
|
||||||
}
|
}
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
Column,
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
DeleteDateColumn,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
|
} from 'typeorm'
|
||||||
|
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
|
||||||
|
import { User } from '../User'
|
||||||
|
import { ContributionMessage } from '../ContributionMessage'
|
||||||
|
|
||||||
|
@Entity('contributions')
|
||||||
|
export class Contribution extends BaseEntity {
|
||||||
|
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
@Column({ type: 'datetime', nullable: false, name: 'contribution_date' })
|
||||||
|
contributionDate: Date
|
||||||
|
|
||||||
|
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||||
|
memo: string
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'decimal',
|
||||||
|
precision: 40,
|
||||||
|
scale: 20,
|
||||||
|
nullable: false,
|
||||||
|
transformer: DecimalTransformer,
|
||||||
|
})
|
||||||
|
amount: Decimal
|
||||||
|
|
||||||
|
@Column({ unsigned: true, nullable: true, name: 'moderator_id' })
|
||||||
|
moderatorId: number
|
||||||
|
|
||||||
|
@Column({ unsigned: true, nullable: true, name: 'contribution_link_id' })
|
||||||
|
contributionLinkId: number
|
||||||
|
|
||||||
|
@Column({ unsigned: true, nullable: true, name: 'confirmed_by' })
|
||||||
|
confirmedBy: number
|
||||||
|
|
||||||
|
@Column({ nullable: true, name: 'confirmed_at' })
|
||||||
|
confirmedAt: Date
|
||||||
|
|
||||||
|
@Column({ unsigned: true, nullable: true, name: 'denied_by' })
|
||||||
|
deniedBy: number
|
||||||
|
|
||||||
|
@Column({ nullable: true, name: 'denied_at' })
|
||||||
|
deniedAt: Date
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'contribution_type',
|
||||||
|
length: 12,
|
||||||
|
nullable: false,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
contributionType: string
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'contribution_status',
|
||||||
|
length: 12,
|
||||||
|
nullable: false,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
contributionStatus: string
|
||||||
|
|
||||||
|
@Column({ unsigned: true, nullable: true, name: 'transaction_id' })
|
||||||
|
transactionId: number
|
||||||
|
|
||||||
|
@DeleteDateColumn({ name: 'deleted_at' })
|
||||||
|
deletedAt: Date | null
|
||||||
|
|
||||||
|
@DeleteDateColumn({ unsigned: true, nullable: true, name: 'deleted_by' })
|
||||||
|
deletedBy: number
|
||||||
|
|
||||||
|
@OneToMany(() => ContributionMessage, (message) => message.contribution)
|
||||||
|
@JoinColumn({ name: 'contribution_id' })
|
||||||
|
messages?: ContributionMessage[]
|
||||||
|
|
||||||
|
@Column({ nullable: true, name: 'updated_at' })
|
||||||
|
updatedAt: Date | null
|
||||||
|
}
|
||||||
@ -1 +1 @@
|
|||||||
export { Contribution } from './0051-add_updated_at_to_contributions/Contribution'
|
export { Contribution } from './0052-add_updated_at_to_contributions/Contribution'
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
export { EventProtocol } from './0043-add_event_protocol_table/EventProtocol'
|
export { EventProtocol } from './0050-add_messageId_to_event_protocol/EventProtocol'
|
||||||
|
|||||||
12
database/migrations/0050-add_messageId_to_event_protocol.ts
Normal file
12
database/migrations/0050-add_messageId_to_event_protocol.ts
Normal file
@ -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<Array<any>>) {
|
||||||
|
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<Array<any>>) {
|
||||||
|
await queryFn(`ALTER TABLE \`event_protocol\` DROP COLUMN \`message_id\`;`)
|
||||||
|
}
|
||||||
12
database/migrations/0051-add_delete_by_to_contributions.ts
Normal file
12
database/migrations/0051-add_delete_by_to_contributions.ts
Normal file
@ -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<Array<any>>) {
|
||||||
|
await queryFn(
|
||||||
|
`ALTER TABLE \`contributions\` ADD COLUMN \`deleted_by\` int(10) unsigned DEFAULT NULL;`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||||
|
await queryFn(`ALTER TABLE \`contributions\` DROP COLUMN \`deleted_by\`;`)
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gradido-database",
|
"name": "gradido-database",
|
||||||
"version": "1.12.1",
|
"version": "1.13.1",
|
||||||
"description": "Gradido Database Tool to execute database migrations",
|
"description": "Gradido Database Tool to execute database migrations",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": "https://github.com/gradido/gradido/database",
|
"repository": "https://github.com/gradido/gradido/database",
|
||||||
|
|||||||
198
docu/Concepts/BusinessRequirements/UC_Set_UserAlias.md
Normal file
198
docu/Concepts/BusinessRequirements/UC_Set_UserAlias.md
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
# User Alias
|
||||||
|
|
||||||
|
Mit dem *Alias* für ein User wird zusätzlich ein eindeutiger menschenlesbarer Identifier neben der GradidoID als technischer Identifier eingeführt. Mit beiden Identifiern kann ein User eindeutig identifziert werden und beide können als Key nach aussen gegeben bzw. für Schnittstellen als Eingabe-Parameter in die Anwendung verwendet werden.
|
||||||
|
|
||||||
|
Ziel dieser beiden Identifier ist von dem bisherig verwendeten Email-Identifier wegzukommen. Denn eine Email-Adresse hat für einen User einen anderen schützenswerten Privatsphären-Level als ein *Alias* oder die *GradidoID*, die beide rein auf die Gradido-Anwendung begrenzt sind. Über Email-Adressen wird gerne eine Social Engineering betrieben, um über einen User ein Profil aus den Social-Media-Netzwerken zu erstellen. Dies gilt es so weit wie möglich zu verhindern bzw. Gradido aus diesem Kreis der möglichen Datenquellen für Ausspähungen von Privatdaten herauszuhalten.
|
||||||
|
|
||||||
|
## Identifizierung eines Users
|
||||||
|
|
||||||
|
In der Gradido-Anwendung muss ein User eindeutig identifzierbar sein. Zur Identifikation eines Users gibt es unterschiedliche Anforderungen:
|
||||||
|
|
||||||
|
* eindeutiger Schlüsselwert für einen User
|
||||||
|
* leicht für den Anwender zu merken
|
||||||
|
* einfache maschinelle Verarbeitung im System
|
||||||
|
* leichte Weitergabe des Schlüsselwertes ausserhalb des Systems
|
||||||
|
* u.a.
|
||||||
|
|
||||||
|
### Schlüsselwerte
|
||||||
|
|
||||||
|
Die hier aufgeführten Schlüsselwerte dienen in der Gradido-Anwendung zur eindeutigen Identifzierung eines Users:
|
||||||
|
|
||||||
|
#### UserID
|
||||||
|
|
||||||
|
Dies ist ein rein technischer Key und wird nur **innerhalb** der Anwendung zur Identifikation eines Users verwendet. Dieser Key wird niemals nach aussen gereicht und auch niemals zwischen mehreren Communities als Schlüsselwert eingesetzt oder ausgetauscht. Die UserID wird innerhalb des Systems bei der Registrierung mit dem Speichern eines neuen Users in der Datenbank erzeugt. Die Eindeutigkeit der UserID ist damit nur innerhalb dieser einen Datenbank der Gradido-Community sichergestellt.
|
||||||
|
|
||||||
|
#### GradidoID
|
||||||
|
|
||||||
|
Die GradidoID ist zwar auch ein rein technischer Key, doch wird dieser als eine UUID der Version 4 erstellt. Dies basiert auf einer (pseudo)zufällig generierten Zahl aus 16 Bytes mit einer theoretischen Konfliktfreiheit von  in hexadezimaler Notation nach einem Pattern von fünf Gruppen durch Bindestrich getrennt - z.B. `550e8400-e29b-41d4-a716-446655440000`
|
||||||
|
|
||||||
|
Somit kann die GradidoID auch System übergreifend zwischen Communities ausgetauscht werden und bietet dennoch eine weitestgehende eindeutige theoretisch konfliktfreie Identifikation des Users. System intern ist die Eindeutigkeit bei der Erstellung eines neuen Users auf jedenfall sichergestellt. Sollte ein User den Wechsel von einer Community in eine andere gradido-Community wünschen, so soll falls möglich die GradidoID für den User erhalten bleiben und übernommen werden können. Dies muss beim Umzug in der Ziel-Community geprüft werden. Falls diese GradidoID aus der Quell-Community wider erwarten existieren sollte, dann muss doch einen neue GradidoID für den User erzeugt werden.
|
||||||
|
|
||||||
|
#### Alias
|
||||||
|
|
||||||
|
Der Alias eines Users ist als rein fachlicher Key ausgelegt, der frei vom User definiert werden kann. Bei der Definition dieses frei definierbaren und menschenlesbaren Schlüsselwertes stellt die Gradido-Anwendung sicher, dass der vom User eingegebene Wert nicht schon von einem anderen User dieser Community verwendet wird. Für die Anlage eines Alias gelten folgende Konventionen:
|
||||||
|
|
||||||
|
- mindestens 5 Zeichen
|
||||||
|
* alphanumerisch
|
||||||
|
* keine Umlaute
|
||||||
|
* nach folgender Regel erlaubt (RegEx: [a-zA-Z0-9]-|_[a-zA-Z0-9])
|
||||||
|
- Blacklist für Schlüsselworte, die frei definiert werden können
|
||||||
|
- vordefinierte/reservierte System relevante Namen dürfen maximal aus 4 Zeichen bestehen
|
||||||
|
|
||||||
|
#### Email
|
||||||
|
|
||||||
|
Die Email eines Users als fachlicher Key bleibt zwar weiterhin bestehen, doch wird diese schrittweise durch die GradidoID und den Alias in den verschiedenen Anwendungsfällen des Gradido-Systems ersetzt. Das bedeutet zum Beispiel, dass die bisher alleinige Verwendung der Email für die Registrierung bzw. den Login nun und durch die GradidoID bzw. den Alias ergänzt wird.
|
||||||
|
|
||||||
|
Die Email wird weiterhin als Kommunikationskanal ausserhalb der Gradido-Anwendung mit dem User benötigt. Es soll aber zukünftig möglich sein, dass ein User ggf. mehrere Email-Adressen für unterschiedliche fachliche Kommunikationskanäle angeben kann. Eine dieser Email-Adressen muss aber als primäre Email-Adresse gekennzeichnet sein, da diese wie bisher auch als Identifier beim Login bzw. der Registrierung erhalten bleiben soll.
|
||||||
|
|
||||||
|
## Erfassung des Alias
|
||||||
|
|
||||||
|
Die Erfassung des Alias erfolgt als zusätzliche Eingabe direkt bei der Registrierung eines neuen Users oder als weiterer Schritt direkt nach dem Login.
|
||||||
|
|
||||||
|
Dieser UseCase ist in die **Ausbaustufe-1** und **Ausbaustufe-x** unterteilt.
|
||||||
|
|
||||||
|
Alle beschriebenen Anforderungen der **Ausbaustufe-1** können mit Produktivsetzung des Issues #1798 - [GradidoID 1: adapt and migrate database schema](https://github.com/gradido/gradido/issues/1798) und dem [PR #2058 - GradidoID 1: adapt and migrate database schema](https://github.com/gradido/gradido/pull/2058) umgesetzt werden.
|
||||||
|
|
||||||
|
Die beschriebenen Anforderungen der Ausbaustufe-x müssen solange verschoben werden, bis der UseCase "GradidoID 2: changeRegisterLoginProzess" konzeptioniert und umgesetzt ist. Erst dann kann auf der Profil-Seite die Email zur Bearbeitung durch den User freigegeben werden.
|
||||||
|
|
||||||
|
### Registrierung
|
||||||
|
|
||||||
|
#### Ausbaustufe-1
|
||||||
|
|
||||||
|
In der Eingabemaske der Registrierung wird nun zusätzlich das Feld *Alias* angezeigt, das der User als Pflichtfeld ausfüllen muss.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Mit dem (optionalen ?) Button "Eindeutigkeit prüfen" wird dem User die Möglichkeit gegeben vorab die Eindeutigkeit seiner *Alias*-Eingabe zu verifizieren ohne den Dialog über den "Registrieren"-Button zu verlassen. Denn es muss sichergestellt sein, dass noch kein existierender User der Community genau diesen *Alias* evtl. schon verwendet.
|
||||||
|
|
||||||
|
Wird diese Prüfung vom User nicht ausgeführt bevor er den Dialog mit dem "Registrieren"-Button abschließt, so erfolgt die *Alias*-Eindeutigkeitsprüfung als erster Schritt bevor die anderen Eingaben als neuer User geprüft und angelegt werden.
|
||||||
|
|
||||||
|
Wird bei der Eindeutigkeitsprüfung des *Alias* festgestellt, dass es schon einen exitierenden User mit dem gleichen *Alias* gibt, dann wird wieder zurück in den Registrierungsdialog gesprungen, damit der User seine *Alias*-Eingabe korrigieren kann. Das *Alias*-Feld wird als fehlerhaft optisch markiert und mit einer aussagekräftigen Fehlermeldung dem User der *Alias*-Konflikt mitgeteilt. Dabei bleiben alle vorher eingegebenen Daten in den Eingabefeldern erhalten und es muss nur der *Alias* geändert werden.
|
||||||
|
|
||||||
|
Wurde vom User nun eine konfliktfreie *Alias*-Eingabe und alle Angaben der Registrierung ordnungsgemäß ausgefüllt, so kann der Registrierungsprozess wie bisher ausgeführt werden. Einziger Unterschied ist der zusätzliche *Alias*-Parameter, der nun an das Backend zur Erzeugung des Users übergeben und dann in der Users-Tabelle gespeichert wird.
|
||||||
|
|
||||||
|
Falls über ein Redeem-Link in den Registrierungsdialog eingestiegen wurde, so bleiben die existierenden Schritte zur Redeem-Link-Verarbeitung durch die *Alias*-Eingabe erhalten und werden unverändert durchlaufen.
|
||||||
|
|
||||||
|
### Login
|
||||||
|
|
||||||
|
#### Ausbaustufe-1
|
||||||
|
|
||||||
|
Meldet sich ein schon registrierter User erfolgreich an - das passiert wie bisher noch mit seiner Email-Adresse - dann wird geprüft, ob für diesen User schon ein *Alias* gespeichert, sprich im aktuellen Context nach dem Login im User-Objekt das Attribut *alias* initialisiert ist. Wenn nicht dann erfolgt direkt nach dem Schließen des Login-Dialoges die Anzeige der User-Profilseite.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Auf der erweiterten User-Profil-Seite sind folgende Elemente neu hinzugekommen bzw. erweitert:
|
||||||
|
|
||||||
|
* *Alias*: **neu hinzugekommen** ist die Gruppe Alias mit dem Label, dem Eingabefeld und dem Link "Alias ändern" mit Stift-Icon
|
||||||
|
* E-Mail: **ergänzt** wurde die Gruppe E-Mail, in dem das Label "E-Mail", das Eingabefeld, das Label "bestätigt" mit zugehörigem Icon darunter und dem Link "E-Mail ändern" mit Stift-Icon. In der **Ausbaustufe-1** ist der Link "E-Mail ändern" und das Stift-Icon **immer disabled**, so dass das Eingabefeld der E-Mail lediglich zur Anzeige der aktuell gesetzten E-Mail-Addresse dient. Da mit der Änderungsmöglichkeit der E-Mail gleichzeitig auch der Login-Prozess und die Passwort-Verschlüsselung angepasst werden muss, wird dieses Feature auf eine spätere Ausbaustufe verschoben. **Neu hinzugekommen** ist die Status-Anzeige der Email-Bestätigung ausgedrückt durch das Häckchen-Icon, wenn die Email-Adresse durch die Email-Confirmation-Mail vom User schon bestätigt ist und durch ein Kreuz-Icon, wenn die Email-Adress-Bestätigung noch aussteht.
|
||||||
|
|
||||||
|
Der Sprung nach der Login-Seite nach erfolgreichem Login auf die Profil-Seite öffnet diese schon direkt im Bearbeitungs-Modus des Alias, so dass der User direkt seine Eingabe des Alias vornehmen kann.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Im Eingabe-Modus der Alias-Gruppe hat das Eingabefeld den Fokus und darin wird:
|
||||||
|
|
||||||
|
* wenn noch kein Alias für den User in der Datenbank vorhanden ist, vom System ein Vorschlag unterbreitet. Der Vorschlag basiert auf dem Vornamen des Users und wird durch folgende Logik ermittelt:
|
||||||
|
* es wird mit dem Vorname des Users eine Datenbankabfrage durchgeführt, die zählt, wieviele User-Aliase es schon mit diesem Vornamen gibt und falls notwendig direkt mit einer nachfolgenden Nummer als Postfix versehen sind.
|
||||||
|
* Aufgrund der Konvention, dass ein Alias mindestens 5 Zeichen lang sein muss, sind ggf. führende Nullen mitzuberücksichten.
|
||||||
|
* **Beispiel-1**: *Max* als Vorname
|
||||||
|
* in der Datenbank gibt es schon mehrer User mit den Aliasen: *Maximilian*, *Max01*, *Max_M*, *Max-M*, *MaxMu* und *Max02*.
|
||||||
|
* Dann schlägt das System den Alias *Max03* vor, da *Max* nur 3 Zeichen lang ist und es schon zwei Aliase *Max* gefolgt mit einer Nummer gibt (*Max01* und *Max02*)
|
||||||
|
* Die Aliase *Maximilian*, *Max_M*, *Max-M* und *MaxMu* werden nicht mitgezählt, das diese nach *Max* keine direkt folgende Ziffern haben
|
||||||
|
* **Beispiel-2**: *August* als Vorname
|
||||||
|
* in der Datenbank gibt es schon mehrer User mit den Aliasen: *Augusta*, *Augustus*, *Augustinus*
|
||||||
|
* Dann schlägt das System den Alias *August* vor, da *August* schon 6 Zeichen lang ist und es noch keinen anderen User mit Alias *August* gibt
|
||||||
|
* die Aliase *Augusta*, *Augustus* und *Augustinus* werden nicht mit gezählt, da diese länger als 5 Zeichen sind und sich von *August* unterscheiden
|
||||||
|
* **Beispiel-3**: *Nick* als Vorname
|
||||||
|
* in der Datenbank gibt es schon mehrer User mit den Aliasen: *Nicko*, *Nickodemus*
|
||||||
|
* Dann schlägt das System den Alias *Nick1* vor, da *Nick* kürzer als 5 Zeichen ist und es noch keinen anderen User mit dem Alias *Nick1* gibt
|
||||||
|
* die Aliase *Nicko* und *Nickodemus* werden nicht mit gezählt, da diese länger als 5 Zeichen sind und sich von *Nick* unterscheiden
|
||||||
|
* wenn schon ein Alias für den User in der Datenbank vorhanden ist, dann wird dieser unverändert aus der Datenbank und ohne Systemvorschlag einfach angezeigt.
|
||||||
|
|
||||||
|
Der User kann nun den im Eingabefeld angezeigten Alias verändern, wobei die Alias-Konventionen, wie oben im ersten Kapitel beschrieben einzuhalten und zu validieren sind.
|
||||||
|
|
||||||
|
Mit dem Button "Eindeutigkeit prüfen" kann der im Eingabefeld stehende *Alias* auf Eindeutigkeit verifziert werden. Dabei wird dieser als Parameter einem Datenbank-Statement übergeben, das auf das Feld *Alias* in der *Users*-Tabelle ein Count mit dem übergebenen Parameter durchführt. Kommt als Ergebnis =0 zurück, ist der eingegebene *Alias* noch nicht vorhanden und kann genutzt werden. Liefert das Count-Statement einen Wert >0, dann ist dieser *Alias* schon von einem anderen User in Gebrauch und darf nicht gespeichert werden. Der User muss also seinen *Alias* erneut ändern.
|
||||||
|
|
||||||
|
Mit dem "Speichern"-Button wird die Eindeutigkeitsprüfung erneut implizit durchgeführt, um sicherzustellen, dass keine *Alias*-Konflikte in der Datenbank gespeichert werden. Sollte wider erwarten doch ein Konflikt bei der Eindeutigkeitsprüfung auftauchen, so bleibt der Dialog im Eingabe-Modus des *Alias* geöffnet und zeigt dem User eine aussagekräftige Fehlermeldung an.
|
||||||
|
|
||||||
|
Über das rote Icon (x) hinter dem Label "Alias ändern" kann die Eingabe bzw. das Ändern des Alias abgebrochen werden.
|
||||||
|
|
||||||
|
Die erweiterte Gruppe E-Mail bleibt immer im Anzeige-Modus und kann selbst über den Link "E-Mail ändern" und das Stift-Icon, die beide disabled sind, nicht in den Bearbeitungsmodus versetzt werden. Die aktuell gesetzte E-Mail des Users wird im disabled Eingabefeld nur angezeigt. Das Icon unter dem Label "bestätigt" zeigt den Status der E-Mail, ob diese schon vom User bestätigt wurde oder nicht. Der Schalter für das Label "Informationen per E-Mail" bleibt von dem Switch zwischen Anzeige-Modus und Bearbeitungs-Modus unberührt, dh. es kann zu jeder Zeit vom User definiert werden, ob er über die gesetzte E-Mail Informationen erhält oder nicht.
|
||||||
|
|
||||||
|
Es gibt in dieser Ausbaustufe-1 noch keine Möglichkeit seine E-Mail-Adresse zu ändern, da vorher die Passwort-Verschlüsselung mit allen Auswirkungen auf den Registrierungs- und Login-Prozess umgebaut werden müssen.
|
||||||
|
|
||||||
|
#### Ausbaustufe-x
|
||||||
|
|
||||||
|
In der weiteren Ausbaustufe, die erst möglich ist, sobald der Login-Prozess und die Passwort-Verschlüsselung darauf umgestellt ist, wird der Link "E-Mail ändern" und das Sift-Icon enabled. Damit kann der User dann das E-Mail Eingabefeld in den Bearbeitungs-Modus versetzen.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Im Eingabe-Modus des E-Mail-Feldes kann der User seine E-Mail-Adresse ändern. Sobald der User die vorhandene und schon bestätigte Email-Adresse ändert, wechselt die Anzeige des Icons unter dem Label "bestätigt" vom Icon (Hacken) zum Icon (X), um die Änderung dem User gleich sichtbar zu machen. Über den Button "Speichern & Bestätigen" wird die veränderte E-Mail gegenüber den bisher gespeicherten E-Mails aller User verifiziert, dass es keine Dupletten gibt.
|
||||||
|
|
||||||
|
Ist diese Eindeutigkeits-Prüfung erfolgreich, dann wird die geänderte E-Mail-Adresse in der Datenbank gespeichert, das Flag E-Mail-Checked auf FALSE gesetzt, damit das Bestätigt-Icon von "bestätigt" auf "unbestätigt" dem User angezeigt wird und zurück in den Anzeige-Modus der Gruppe E-Mail gewechselt. Mit der Speicherung der geänderten E-Mail wird eine Comfirmation-Email an diese E-Mail-Adresse zur Bestätigung durch den User gesendet.
|
||||||
|
|
||||||
|
Ist diese Prüfung fehlgeschlagen, sprich es gibt die zuspeichernde E-Mail-Adresse schon in der Datenbank, dann wird das Speichern der geänderten E-Mail abgebrochen und es bleibt die zuvor gespeicherte E-Mail gültig und auch das E-Mail-Checked Flag bleibt auf dem vorherigen Status. Ob und welche Meldung dem User in dieser Situation angezeigt wird, ist noch zu definieren, um kein Ausspionieren von anderen E-Mail-Adressen zu unterstützen. Ebenfalls noch offen ist, ob an die gefundene E-Mail-Duplette eine Info-Email geschickt wird, um den User, der diese bestätigte E-Mail-Adresse besitzt, zu informieren, dass es einen Versuch gab seine E-Mail zu verwenden.
|
||||||
|
|
||||||
|
## Backend-Services
|
||||||
|
|
||||||
|
### UserResolver.createUser - erweitern
|
||||||
|
|
||||||
|
#### Ausbaustufe-1
|
||||||
|
|
||||||
|
Der Service *createUser* wird um den Pflicht-Parameter *alias: String* erweitert. Der Wert wurde, wie oben beschrieben, im Dialog Register erfasst und gemäß den Konventionen für das Feld *alias* auch validiert - Länge und erlaubte Zeichen.
|
||||||
|
|
||||||
|
Es wird vor jeder anderen Aktion die Eindeutigkeitsprüfung des übergebenen alias-Wertes geprüft. Dazu wird der neue Service verifyUniqueAlias() im UserResolver aufgerufen, der auch direkt vom Frontend aufgerufen werden kann.
|
||||||
|
|
||||||
|
Liefert diese Prüfung den Wert FALSE, dann wird das Anlegen und Speichern des neuen Users abgebrochen und mit entsprechend aussagekräftiger Fehlermeldung, dass der Alias nicht eindeutig ist, an das Frontend zurückgegeben.
|
||||||
|
|
||||||
|
Ist die Eindeutigkeitsprüfung hingegen erfolgreich, dann wird die existierende Logik zur Anlage eines neuen Users weiter ausgeführt. Dabei ist der neue Parameter *alias* in den neu angelegten User zu übertragen und in der Datenbank zu speichern.
|
||||||
|
|
||||||
|
Alle weiteren Ausgabe-Kanäle wie Logging, EventProtokoll und Emails sind entsprechend einzubauen, aber mindestens um das neue Attribut *alias* zu ergänzen.
|
||||||
|
|
||||||
|
### UserResolver.verifyUniqueAlias - neu
|
||||||
|
|
||||||
|
#### Ausbaustufe-1
|
||||||
|
|
||||||
|
Dieser neue Service bekommt als Parameter das Attribut *alias: String* übergeben und liefert im Ergebnis TRUE, wenn der übergebene *alias* noch nicht in der Datenbank von einem anderen User verwendet wird, andernfalls FALSE.
|
||||||
|
|
||||||
|
Dabei wird ein einfaches Datenbank-Statement auf die *Users* Tabelle abgesetzt mit einem casesensitiven Vergleich auf den Parameter mit den Werten aus der Spalte *alias*
|
||||||
|
|
||||||
|
`SELECT count(*) FROM users where BINARY users.alias = {alias}`
|
||||||
|
|
||||||
|
### UserResolver.updateUserInfos - erweitern
|
||||||
|
|
||||||
|
#### Ausbaustufe-1
|
||||||
|
|
||||||
|
Der schon existierende Service *updateUserInfos()* wird erweitert um den Parameter *alias: String*. Sobald der User nach dem Login automatisch oder selbst interaktiv auf die Profil-Seite navigiert und dort sein Profil, insbesonderen neu das Attribut *alias* erfasst oder ändert, wird dieser Service aufgerufen.
|
||||||
|
|
||||||
|
Die Parameter *firstName, lastName, language, password, passwordNew, alias* werden alle als optional definiert, da der User auf der Profil-Seite auswählen kann, welche Profil-Parameter er verändern möchte und somit meist nie alle Parameter gleichzeitig dieses Service initialisiert sind.
|
||||||
|
|
||||||
|
Sobald der *alias*-Parameter gesetzt ist, wird für diesen der Service *verifyUniqueAlias()* zur Eindeutigekeitsprüfung aufgerufen, um sicherzustellen, dass der übergebene *alias* wirklich nicht schon in der Datenbank existiert. Liefert das Ergebnis von *verifyUniqueAlias()* den Wert TRUE, dann kann der übergebene *alias* in der Datenbank gespeichert werden. Anderfalls muss mit einer aussagekräftigen Fehlermeldung abgebrochen werden und es wird keiner der übergebenen Parameter in die Datenbank geschrieben.
|
||||||
|
|
||||||
|
Alle weiteren Ausgabe-Kanäle wie Logging, EventProtokoll sind entsprechend einzubauen, aber mindestens um das neue Attribut *alias* zu ergänzen.
|
||||||
|
|
||||||
|
#### Ausbaustufe-x
|
||||||
|
|
||||||
|
Sobald in einer weiteren Ausbaustufe die Email auf der Profil-Seite vom User verändert werden kann, dann wird dieser Service um den optionalen Parameter *email: String* erweitert.
|
||||||
|
|
||||||
|
Sobald der *email*-Parameter gesetzt ist, wird für diesen der Service *verifyUniqueEmail()* zur Eindeutigekeitsprüfung aufgerufen, um sicherzustellen, dass die übergebene *email* wirklich nicht schon für einen anderen User in der Datenbank existiert. Liefert das Ergebnis von *verifyUniqueEmail()* den Wert TRUE, dann kann die übergebene *email* in der Datenbank gespeichert werden. Anderfalls muss mit einer aussagekräftigen Fehlermeldung abgebrochen werden und es wird keiner der übergebenen Parameter in die Datenbank geschrieben.
|
||||||
|
|
||||||
|
Mit dem Speichern der geänderten Email muss auch das Flag *emailChecked* auf FALSE gesetzt und gespeichert werden. Damit wird sichergestellt, dass die veränderte Email-Adresse erst noch vom User bestätigt werden muss. Dies wird direkt nach dem Speichern der Email-Adresse mit dem Versenden einer *confirmChangedEmail* an die neue Email-Adresse initiiert. Der darin enthaltene Bestätigungs-Link wird analog dem Aktivierungs-Link bei der Registrierung der Email gehandhabt. Die *confirmChangedEmail* muss nur inhaltlich vom Text anders formuliert werden als die *AccountActivation*-Email, aber bzgl. der Parameter und des enthaltenen Bestätigungslinks unterscheiden sich beide nicht.
|
||||||
|
|
||||||
|
Sobald der User in seiner erhaltenen *confirmChangedEmail* den Link aktiviert, erfolgt der Aufruf des Service *UserResolver.queryOptIn*, um zu prüfen, ob der in dem Link enthaltene OptInCode valide und gültig ist. Falls ja, dann wird das Flag *emailChecked* auf TRUE gesetzt, anderfalls bleibt es auf FALSE und es wird mit einer aussagekräftigen Fehlermeldung abgebrochen.
|
||||||
|
|
||||||
|
Alle weiteren Ausgabe-Kanäle wie Logging, EventProtokoll sind entsprechend einzubauen, aber mindestens um das neue Attribut *email* zu ergänzen.
|
||||||
|
|
||||||
|
### UserResolver.verifyUniqueEmail - neu
|
||||||
|
|
||||||
|
#### Ausbaustufe-x
|
||||||
|
|
||||||
|
Dieser neue Service bekommt als Parameter das Attribut *email: String* übergeben und liefert im Ergebnis TRUE, wenn die übergebene *email* noch nicht in der Datenbank von einem anderen User verwendet wird, andernfalls FALSE.
|
||||||
|
|
||||||
|
Dabei wird ein einfaches Datenbank-Statement auf die *Users* Tabelle abgesetzt mit einem casesensitiven Vergleich auf den Parameter mit den Werten aus der Spalte *email*
|
||||||
|
|
||||||
|
`SELECT count(*) FROM users where BINARY users.email = {email}`
|
||||||
|
|
||||||
|
## Datenbank-Migration
|
||||||
|
|
||||||
|
Es ist für diesen UseCase keine Datenbank-Migration notwendig, da im Rahmen der Einführung der GradidoID die Spalte *alias* schon in die *Users*-Tabelle mit aufgenommen wurde.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
BIN
docu/Concepts/BusinessRequirements/image/LoginWithAlias.png
Normal file
BIN
docu/Concepts/BusinessRequirements/image/LoginWithAlias.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
BIN
docu/Concepts/BusinessRequirements/image/RegisterWithAlias.png
Normal file
BIN
docu/Concepts/BusinessRequirements/image/RegisterWithAlias.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bootstrap-vue-gradido-wallet",
|
"name": "bootstrap-vue-gradido-wallet",
|
||||||
"version": "1.12.1",
|
"version": "1.13.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node run/server.js",
|
"start": "node run/server.js",
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export const transactionsQuery = gql`
|
|||||||
linkedUser {
|
linkedUser {
|
||||||
firstName
|
firstName
|
||||||
lastName
|
lastName
|
||||||
|
email
|
||||||
}
|
}
|
||||||
decay {
|
decay {
|
||||||
decay
|
decay
|
||||||
@ -44,9 +45,6 @@ export const transactionsQuery = gql`
|
|||||||
end
|
end
|
||||||
duration
|
duration
|
||||||
}
|
}
|
||||||
linkedUser {
|
|
||||||
email
|
|
||||||
}
|
|
||||||
transactionLinkId
|
transactionLinkId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,7 +34,7 @@
|
|||||||
"contribution": {
|
"contribution": {
|
||||||
"activity": "Tätigkeit",
|
"activity": "Tätigkeit",
|
||||||
"alert": {
|
"alert": {
|
||||||
"answerQuestion": "Bitte beantworte die Nachfrage",
|
"answerQuestion": "Bitte beantworte die Rückfrage!",
|
||||||
"communityNoteList": "Hier findest du alle eingereichten und bestätigten Beiträge von allen Mitgliedern aus dieser Gemeinschaft.",
|
"communityNoteList": "Hier findest du alle eingereichten und bestätigten Beiträge von allen Mitgliedern aus dieser Gemeinschaft.",
|
||||||
"confirm": "bestätigt",
|
"confirm": "bestätigt",
|
||||||
"in_progress": "Es gibt eine Rückfrage der Moderatoren.",
|
"in_progress": "Es gibt eine Rückfrage der Moderatoren.",
|
||||||
|
|||||||
@ -231,8 +231,6 @@ export default {
|
|||||||
this.items = listContributions.contributionList
|
this.items = listContributions.contributionList
|
||||||
if (this.items.find((item) => item.state === 'IN_PROGRESS')) {
|
if (this.items.find((item) => item.state === 'IN_PROGRESS')) {
|
||||||
this.tabIndex = 1
|
this.tabIndex = 1
|
||||||
} else {
|
|
||||||
this.tabIndex = 0
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gradido",
|
"name": "gradido",
|
||||||
"version": "1.12.1",
|
"version": "1.13.1",
|
||||||
"description": "Gradido",
|
"description": "Gradido",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"repository": "git@github.com:gradido/gradido.git",
|
"repository": "git@github.com:gradido/gradido.git",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user