+
@@ -104,7 +104,6 @@
:contributionId="row.item.id"
:contributionState="row.item.state"
@update-state="updateState"
- @update-user-data="updateUserData"
/>
@@ -146,22 +145,9 @@ export default {
required: true,
},
},
- data() {
- return {
- creationUserData: {
- amount: null,
- date: null,
- memo: null,
- moderator: null,
- },
- }
- },
methods: {
myself(item) {
- return (
- `${item.firstName} ${item.lastName}` ===
- `${this.$store.state.moderator.firstName} ${this.$store.state.moderator.lastName}`
- )
+ return item.userId === this.$store.state.moderator.id
},
getStatusIcon(status) {
return iconMap[status] ? iconMap[status] : 'default-icon'
@@ -174,16 +160,6 @@ export default {
if (item.state === 'IN_PROGRESS') return 'table-primary'
if (item.state === 'PENDING') return 'table-primary'
},
- updateCreationData(data) {
- const row = data.row
- this.$emit('update-contributions', data)
- delete data.row
- this.creationUserData = { ...this.creationUserData, ...data }
- row.toggleDetails()
- },
- updateUserData(rowItem, newCreation) {
- rowItem.creation = newCreation
- },
updateState(id) {
this.$emit('update-state', id)
},
diff --git a/admin/src/graphql/adminListContributions.js b/admin/src/graphql/adminListContributions.js
index 9d63bf6a9..97a11c61a 100644
--- a/admin/src/graphql/adminListContributions.js
+++ b/admin/src/graphql/adminListContributions.js
@@ -32,6 +32,8 @@ export const adminListContributions = gql`
deniedBy
deletedAt
deletedBy
+ moderatorId
+ userId
}
}
}
diff --git a/admin/src/graphql/adminOpenCreations.js b/admin/src/graphql/adminOpenCreations.js
new file mode 100644
index 000000000..0e766c0f7
--- /dev/null
+++ b/admin/src/graphql/adminOpenCreations.js
@@ -0,0 +1,11 @@
+import gql from 'graphql-tag'
+
+export const adminOpenCreations = gql`
+ query ($userId: Int!) {
+ adminOpenCreations(userId: $userId) {
+ year
+ month
+ amount
+ }
+ }
+`
diff --git a/admin/src/graphql/adminUpdateContribution.js b/admin/src/graphql/adminUpdateContribution.js
index b7c834109..c52a0cbc4 100644
--- a/admin/src/graphql/adminUpdateContribution.js
+++ b/admin/src/graphql/adminUpdateContribution.js
@@ -1,18 +1,11 @@
import gql from 'graphql-tag'
export const adminUpdateContribution = gql`
- mutation ($id: Int!, $email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
- adminUpdateContribution(
- id: $id
- email: $email
- amount: $amount
- memo: $memo
- creationDate: $creationDate
- ) {
+ mutation ($id: Int!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
+ adminUpdateContribution(id: $id, amount: $amount, memo: $memo, creationDate: $creationDate) {
amount
date
memo
- creation
}
}
`
diff --git a/admin/src/locales/de.json b/admin/src/locales/de.json
index 0d05eaa88..f98dc90f3 100644
--- a/admin/src/locales/de.json
+++ b/admin/src/locales/de.json
@@ -1,6 +1,7 @@
{
"all_emails": "Alle Nutzer",
"back": "zurück",
+ "change_user_role": "Nutzerrolle ändern",
"chat": "Chat",
"contributionLink": {
"amount": "Betrag",
@@ -114,6 +115,11 @@
"open_creations": "Offene Schöpfungen",
"overlay": {
"cancel": "Abbrechen",
+ "changeUserRole": {
+ "question": "Willst du die Rolle von {username} wirklich zu {newRole} ändern?",
+ "title": "Nutzerrolle ändern",
+ "yes": "Ja, Nutzerrolle ändern"
+ },
"confirm": {
"question": "Willst du diesen Gemeinwohl-Beitrag wirklich bestätigen und gutschreiben?",
"text": "Nach dem Speichern ist der Datensatz nicht mehr änderbar. Bitte überprüfe genau, dass alles stimmt.",
@@ -126,11 +132,21 @@
"title": "Gemeinwohl-Beitrag löschen!",
"yes": "Ja, Beitrag löschen!"
},
+ "deleteUser": {
+ "question": "Willst du {username} wirklich löschen?",
+ "title": "Nutzer löschen",
+ "yes": "Ja, Nutzer löschen"
+ },
"deny": {
"question": "Willst du diesen Gemeinwohl-Beitrag wirklich ablehnen?",
"text": "Nach dem Speichern ist der Datensatz nicht mehr änderbar und kann auch nicht mehr gelöscht werden. Bitte überprüfe genau, dass alles stimmt.",
"title": "Gemeinwohl-Beitrag ablehnen!",
"yes": "Ja, Beitrag ablehnen und speichern!"
+ },
+ "undeleteUser": {
+ "question": "Willst du wirklich {username} wiederherstellen?",
+ "title": "Nutzer wiederherstellen",
+ "yes": "Ja, Nutzer wiederherstellen"
}
},
"redeemed": "eingelöst",
diff --git a/admin/src/locales/en.json b/admin/src/locales/en.json
index f36944a63..358c62a15 100644
--- a/admin/src/locales/en.json
+++ b/admin/src/locales/en.json
@@ -1,6 +1,7 @@
{
"all_emails": "All users",
"back": "back",
+ "change_user_role": "Change user role",
"chat": "Chat",
"contributionLink": {
"amount": "Amount",
@@ -114,6 +115,11 @@
"open_creations": "Open creations",
"overlay": {
"cancel": "Cancel",
+ "changeUserRole": {
+ "question": "Do you really want to change {username}'s role to {newRole}?",
+ "title": "Change user role",
+ "yes": "Yes, change user role"
+ },
"confirm": {
"question": "Do you really want to carry out and finally save this pre-stored creation?",
"text": "After saving, the record can no longer be changed. Please check carefully that everything is correct.",
@@ -126,11 +132,21 @@
"title": "Delete creation!",
"yes": "Yes, delete and save creation!"
},
+ "deleteUser": {
+ "question": "Do you really want to delete {username}?",
+ "title": "Delete user",
+ "yes": "Yes, delete user"
+ },
"deny": {
"question": "Do you really want to carry out and finally save this pre-stored creation?",
"text": "After saving, the record can no longer be changed or deleted. Please check carefully that everything is correct.",
"title": "Reject creation!",
"yes": "Yes, reject and save creation!"
+ },
+ "undeleteUser": {
+ "question": "Do you really want to undelete {username}",
+ "title": "Undelete user",
+ "yes": "Yes,undelete user"
}
},
"redeemed": "redeemed",
diff --git a/admin/src/mixins/creationMonths.js b/admin/src/mixins/creationMonths.js
index c26dc5b02..57e0ab17e 100644
--- a/admin/src/mixins/creationMonths.js
+++ b/admin/src/mixins/creationMonths.js
@@ -1,9 +1,11 @@
+import { adminOpenCreations } from '../graphql/adminOpenCreations'
+
export const creationMonths = {
- props: {
- creation: {
- type: Array,
- default: () => [1000, 1000, 1000],
- },
+ data() {
+ return {
+ creation: [1000, 1000, 1000],
+ userId: 0,
+ }
},
computed: {
creationDates() {
@@ -38,4 +40,23 @@ export const creationMonths = {
return this.creationDates.map((date) => this.$d(date, 'monthShort')).join(' | ')
},
},
+ apollo: {
+ OpenCreations: {
+ query() {
+ return adminOpenCreations
+ },
+ variables() {
+ return {
+ userId: this.userId,
+ }
+ },
+ fetchPolicy: 'no-cache',
+ update({ adminOpenCreations }) {
+ this.creation = adminOpenCreations.map((obj) => obj.amount)
+ },
+ error({ message }) {
+ this.toastError(message)
+ },
+ },
+ },
}
diff --git a/admin/src/pages/CreationConfirm.vue b/admin/src/pages/CreationConfirm.vue
index b367d1b7f..3c1a1e67e 100644
--- a/admin/src/pages/CreationConfirm.vue
+++ b/admin/src/pages/CreationConfirm.vue
@@ -44,7 +44,7 @@
:fields="fields"
@show-overlay="showOverlay"
@update-state="updateStatus"
- @update-contributions="$apollo.queries.AllContributions.refetch()"
+ @update-contributions="$apollo.queries.ListAllContributions.refetch()"
/>
{
amount: 500,
memo: 'Danke für alles',
date: new Date(),
- moderator: 1,
+ moderatorId: 1,
state: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
@@ -64,7 +64,7 @@ const defaultData = () => {
amount: 1000000,
memo: 'Gut Ergattert',
date: new Date(),
- moderator: 1,
+ moderatorId: 1,
state: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
diff --git a/admin/src/pages/UserSearch.spec.js b/admin/src/pages/UserSearch.spec.js
index 77e8a3559..0d145cb89 100644
--- a/admin/src/pages/UserSearch.spec.js
+++ b/admin/src/pages/UserSearch.spec.js
@@ -25,7 +25,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
email: 'benjamin@bluemchen.de',
creation: [1000, 1000, 1000],
emailChecked: true,
- deletedAt: null,
+ deletedAt: new Date(),
},
{
userId: 3,
@@ -243,6 +243,17 @@ describe('UserSearch', () => {
})
})
+ describe('recover user', () => {
+ const userId = 2
+ beforeEach(() => {
+ wrapper.findComponent({ name: 'SearchUserTable' }).vm.$emit('updateDeletedAt', userId, null)
+ })
+
+ it('toasts a success message', () => {
+ expect(toastSuccessSpy).toBeCalledWith('user_recovered')
+ })
+ })
+
describe('apollo returns error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({
diff --git a/backend/src/graphql/arg/AdminUpdateContributionArgs.ts b/backend/src/graphql/arg/AdminUpdateContributionArgs.ts
index f3cf519b6..6a8f00dc2 100644
--- a/backend/src/graphql/arg/AdminUpdateContributionArgs.ts
+++ b/backend/src/graphql/arg/AdminUpdateContributionArgs.ts
@@ -6,9 +6,6 @@ export default class AdminUpdateContributionArgs {
@Field(() => Int)
id: number
- @Field(() => String)
- email: string
-
@Field(() => Decimal)
amount: Decimal
diff --git a/backend/src/graphql/model/Contribution.ts b/backend/src/graphql/model/Contribution.ts
index b19fae25b..b5db21b16 100644
--- a/backend/src/graphql/model/Contribution.ts
+++ b/backend/src/graphql/model/Contribution.ts
@@ -21,6 +21,8 @@ export class Contribution {
this.deniedBy = contribution.deniedBy
this.deletedAt = contribution.deletedAt
this.deletedBy = contribution.deletedBy
+ this.moderatorId = contribution.moderatorId
+ this.userId = contribution.userId
}
@Field(() => Int)
@@ -67,6 +69,12 @@ export class Contribution {
@Field(() => String)
state: string
+
+ @Field(() => Int, { nullable: true })
+ moderatorId: number | null
+
+ @Field(() => Int, { nullable: true })
+ userId: number | null
}
@ObjectType()
diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts
index 14d3fe73e..490f5a4a7 100644
--- a/backend/src/graphql/resolver/ContributionResolver.test.ts
+++ b/backend/src/graphql/resolver/ContributionResolver.test.ts
@@ -437,7 +437,6 @@ describe('ContributionResolver', () => {
mutation: adminUpdateContribution,
variables: {
id: pendingContribution.data.createContribution.id,
- email: 'bibi@bloxberg.de',
amount: 10.0,
memo: 'Test env contribution',
creationDate: new Date().toString(),
@@ -1672,7 +1671,6 @@ describe('ContributionResolver', () => {
mutation: adminUpdateContribution,
variables: {
id: 1,
- email: 'bibi@bloxberg.de',
amount: new Decimal(300),
memo: 'Danke Bibi!',
creationDate: contributionDateFormatter(new Date()),
@@ -1751,7 +1749,6 @@ describe('ContributionResolver', () => {
mutation: adminUpdateContribution,
variables: {
id: 1,
- email: 'bibi@bloxberg.de',
amount: new Decimal(300),
memo: 'Danke Bibi!',
creationDate: contributionDateFormatter(new Date()),
@@ -2045,6 +2042,50 @@ describe('ContributionResolver', () => {
}),
)
})
+
+ describe('user tries to update admin contribution', () => {
+ beforeAll(async () => {
+ await mutate({
+ mutation: login,
+ variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
+ })
+ })
+
+ afterAll(async () => {
+ await mutate({
+ mutation: login,
+ variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
+ })
+ })
+
+ it('logs and throws "Cannot update contribution of moderator" error', async () => {
+ jest.clearAllMocks()
+ const adminContribution = await Contribution.findOne({
+ where: {
+ moderatorId: admin.id,
+ userId: bibi.id,
+ },
+ })
+ await expect(
+ mutate({
+ mutation: updateContribution,
+ variables: {
+ contributionId: (adminContribution && adminContribution.id) || -1,
+ amount: 100.0,
+ memo: 'Test Test Test',
+ creationDate: new Date().toString(),
+ },
+ }),
+ ).resolves.toMatchObject({
+ errors: [new GraphQLError('Cannot update contribution of moderator')],
+ })
+ expect(logger.error).toBeCalledWith(
+ 'Cannot update contribution of moderator',
+ expect.any(Object),
+ bibi.id,
+ )
+ })
+ })
})
describe('second creation surpasses the available amount ', () => {
@@ -2082,58 +2123,6 @@ describe('ContributionResolver', () => {
// stephen@hawking.uk: [1000, 1000, 1000] - deleted
// garrick@ollivander.com: [1000, 1000, 1000] - not activated
- describe('user for creation to update does not exist', () => {
- it('throws an error', async () => {
- jest.clearAllMocks()
- await expect(
- mutate({
- mutation: adminUpdateContribution,
- variables: {
- id: 1,
- email: 'bob@baumeister.de',
- amount: new Decimal(300),
- memo: 'Danke Bibi!',
- creationDate: contributionDateFormatter(new Date()),
- },
- }),
- ).resolves.toEqual(
- expect.objectContaining({
- errors: [new GraphQLError('Could not find User')],
- }),
- )
- })
-
- it('logs the error "Could not find User"', () => {
- expect(logger.error).toBeCalledWith('Could not find User', 'bob@baumeister.de')
- })
- })
-
- describe('user for creation to update is deleted', () => {
- it('throws an error', async () => {
- jest.clearAllMocks()
- await expect(
- mutate({
- mutation: adminUpdateContribution,
- variables: {
- id: 1,
- email: 'stephen@hawking.uk',
- amount: new Decimal(300),
- memo: 'Danke Bibi!',
- creationDate: contributionDateFormatter(new Date()),
- },
- }),
- ).resolves.toEqual(
- expect.objectContaining({
- errors: [new GraphQLError('User was deleted')],
- }),
- )
- })
-
- it('logs the error "User was deleted"', () => {
- expect(logger.error).toBeCalledWith('User was deleted', 'stephen@hawking.uk')
- })
- })
-
describe('creation does not exist', () => {
it('throws an error', async () => {
jest.clearAllMocks()
@@ -2142,7 +2131,6 @@ describe('ContributionResolver', () => {
mutation: adminUpdateContribution,
variables: {
id: -1,
- email: 'bibi@bloxberg.de',
amount: new Decimal(300),
memo: 'Danke Bibi!',
creationDate: contributionDateFormatter(new Date()),
@@ -2160,40 +2148,6 @@ describe('ContributionResolver', () => {
})
})
- describe('user email does not match creation user', () => {
- it('throws an error', async () => {
- jest.clearAllMocks()
- await expect(
- mutate({
- mutation: adminUpdateContribution,
- variables: {
- id: creation ? creation.id : -1,
- email: 'bibi@bloxberg.de',
- amount: new Decimal(300),
- memo: 'Danke Bibi!',
- creationDate: creation
- ? contributionDateFormatter(creation.contributionDate)
- : contributionDateFormatter(new Date()),
- },
- }),
- ).resolves.toEqual(
- expect.objectContaining({
- errors: [
- new GraphQLError(
- 'User of the pending contribution and send user does not correspond',
- ),
- ],
- }),
- )
- })
-
- it('logs the error "User of the pending contribution and send user does not correspond"', () => {
- expect(logger.error).toBeCalledWith(
- 'User of the pending contribution and send user does not correspond',
- )
- })
- })
-
describe('creation update is not valid', () => {
// as this test has not clearly defined that date, it is a false positive
it('throws an error', async () => {
@@ -2203,7 +2157,6 @@ describe('ContributionResolver', () => {
mutation: adminUpdateContribution,
variables: {
id: creation ? creation.id : -1,
- email: 'peter@lustig.de',
amount: new Decimal(1900),
memo: 'Danke Peter!',
creationDate: creation
@@ -2240,7 +2193,6 @@ describe('ContributionResolver', () => {
mutation: adminUpdateContribution,
variables: {
id: creation?.id,
- email: 'peter@lustig.de',
amount: new Decimal(300),
memo: 'Danke Peter!',
creationDate: creation
@@ -2255,7 +2207,6 @@ describe('ContributionResolver', () => {
date: expect.any(String),
memo: 'Danke Peter!',
amount: '300',
- creation: ['1000', '700', '500'],
},
},
}),
@@ -2282,7 +2233,6 @@ describe('ContributionResolver', () => {
mutation: adminUpdateContribution,
variables: {
id: creation?.id,
- email: 'peter@lustig.de',
amount: new Decimal(200),
memo: 'Das war leider zu Viel!',
creationDate: creation
@@ -2297,7 +2247,6 @@ describe('ContributionResolver', () => {
date: expect.any(String),
memo: 'Das war leider zu Viel!',
amount: '200',
- creation: ['1000', '800', '1000'],
},
},
}),
diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts
index 79e21b49e..5969eaef2 100644
--- a/backend/src/graphql/resolver/ContributionResolver.ts
+++ b/backend/src/graphql/resolver/ContributionResolver.ts
@@ -201,6 +201,9 @@ export class ContributionResolver {
user.id,
)
}
+ if (contributionToUpdate.moderatorId) {
+ throw new LogError('Cannot update contribution of moderator', contributionToUpdate, user.id)
+ }
if (
contributionToUpdate.contributionStatus !== ContributionStatus.IN_PROGRESS &&
contributionToUpdate.contributionStatus !== ContributionStatus.PENDING
@@ -306,41 +309,27 @@ export class ContributionResolver {
@Authorized([RIGHTS.ADMIN_UPDATE_CONTRIBUTION])
@Mutation(() => AdminUpdateContribution)
async adminUpdateContribution(
- @Args() { id, email, amount, memo, creationDate }: AdminUpdateContributionArgs,
+ @Args() { id, amount, memo, creationDate }: AdminUpdateContributionArgs,
@Ctx() context: Context,
): Promise {
const clientTimezoneOffset = getClientTimezoneOffset(context)
- const emailContact = await UserContact.findOne({
- where: { email },
- withDeleted: true,
- relations: ['user'],
- })
- if (!emailContact || !emailContact.user) {
- throw new LogError('Could not find User', email)
- }
- if (emailContact.deletedAt || emailContact.user.deletedAt) {
- throw new LogError('User was deleted', email)
- }
const moderator = getUser(context)
const contributionToUpdate = await DbContribution.findOne({
where: { id, confirmedAt: IsNull(), deniedAt: IsNull() },
})
+
if (!contributionToUpdate) {
throw new LogError('Contribution not found', id)
}
- if (contributionToUpdate.userId !== emailContact.user.id) {
- throw new LogError('User of the pending contribution and send user does not correspond')
- }
-
if (contributionToUpdate.moderatorId === null) {
throw new LogError('An admin is not allowed to update an user contribution')
}
const creationDateObj = new Date(creationDate)
- let creations = await getUserCreation(emailContact.user.id, clientTimezoneOffset)
+ let creations = await getUserCreation(contributionToUpdate.userId, clientTimezoneOffset)
// TODO: remove this restriction
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
@@ -363,9 +352,9 @@ export class ContributionResolver {
result.amount = amount
result.memo = contributionToUpdate.memo
result.date = contributionToUpdate.contributionDate
- result.creation = await getUserCreation(emailContact.user.id, clientTimezoneOffset)
+
await EVENT_ADMIN_CONTRIBUTION_UPDATE(
- emailContact.user,
+ { id: contributionToUpdate.userId } as DbUser,
moderator,
contributionToUpdate,
amount,
diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts
index 1c2473608..7e141e5d6 100644
--- a/backend/src/seeds/graphql/mutations.ts
+++ b/backend/src/seeds/graphql/mutations.ts
@@ -133,18 +133,11 @@ export const unDeleteUser = gql`
`
export const adminUpdateContribution = gql`
- mutation ($id: Int!, $email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
- adminUpdateContribution(
- id: $id
- email: $email
- amount: $amount
- memo: $memo
- creationDate: $creationDate
- ) {
+ mutation ($id: Int!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
+ adminUpdateContribution(id: $id, amount: $amount, memo: $memo, creationDate: $creationDate) {
amount
date
memo
- creation
}
}
`
diff --git a/frontend/src/components/Contributions/ContributionListItem.vue b/frontend/src/components/Contributions/ContributionListItem.vue
index 56546d183..27e8459bd 100644
--- a/frontend/src/components/Contributions/ContributionListItem.vue
+++ b/frontend/src/components/Contributions/ContributionListItem.vue
@@ -47,7 +47,7 @@
{{ amount | GDD }}
-
+
@@ -58,7 +58,7 @@
>
@@ -69,7 +69,7 @@
{{ $t('edit') }}
-
-
+
{{ $t('moderatorChat') }}
@@ -180,6 +179,11 @@ export default {
required: false,
default: false,
},
+ moderatorId: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
data() {
return {
diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js
index 7193eded0..1a37d082b 100644
--- a/frontend/src/graphql/queries.js
+++ b/frontend/src/graphql/queries.js
@@ -187,6 +187,7 @@ export const listContributions = gql`
messagesCount
deniedAt
deniedBy
+ moderatorId
}
}
}
@@ -236,7 +237,7 @@ export const searchAdminUsers = gql`
`
export const listContributionMessages = gql`
- query($contributionId: Float!, $pageSize: Int = 25, $currentPage: Int = 1, $order: Order = ASC) {
+ query($contributionId: Int!, $pageSize: Int = 25, $currentPage: Int = 1, $order: Order = ASC) {
listContributionMessages(
contributionId: $contributionId
pageSize: $pageSize