-
+
@@ -79,6 +79,9 @@
+
+
+
@@ -93,6 +96,7 @@ import CreationFormular from '../CreationFormular.vue'
import ConfirmRegisterMailFormular from '../ConfirmRegisterMailFormular.vue'
import CreationTransactionList from '../CreationTransactionList.vue'
import TransactionLinkList from '../TransactionLinkList.vue'
+import ChangeUserRoleFormular from '../ChangeUserRoleFormular.vue'
import DeletedUserFormular from '../DeletedUserFormular.vue'
export default {
@@ -102,6 +106,7 @@ export default {
ConfirmRegisterMailFormular,
CreationTransactionList,
TransactionLinkList,
+ ChangeUserRoleFormular,
DeletedUserFormular,
},
props: {
@@ -123,6 +128,9 @@ export default {
updateUserData(rowItem, newCreation) {
rowItem.creation = newCreation
},
+ updateIsAdmin({ userId, isAdmin }) {
+ this.$emit('updateIsAdmin', userId, isAdmin)
+ },
updateDeletedAt({ userId, deletedAt }) {
this.$emit('updateDeletedAt', userId, deletedAt)
},
diff --git a/admin/src/graphql/searchUsers.js b/admin/src/graphql/searchUsers.js
index cc7f83b1c..3e33fe96b 100644
--- a/admin/src/graphql/searchUsers.js
+++ b/admin/src/graphql/searchUsers.js
@@ -19,6 +19,7 @@ export const searchUsers = gql`
hasElopage
emailConfirmationSend
deletedAt
+ isAdmin
}
}
}
diff --git a/admin/src/graphql/setUserRole.js b/admin/src/graphql/setUserRole.js
new file mode 100644
index 000000000..8cdcab396
--- /dev/null
+++ b/admin/src/graphql/setUserRole.js
@@ -0,0 +1,7 @@
+import gql from 'graphql-tag'
+
+export const setUserRole = gql`
+ mutation ($userId: Int!, $isAdmin: Boolean!) {
+ setUserRole(userId: $userId, isAdmin: $isAdmin)
+ }
+`
diff --git a/admin/src/locales/de.json b/admin/src/locales/de.json
index 2256c1252..fa0ca6903 100644
--- a/admin/src/locales/de.json
+++ b/admin/src/locales/de.json
@@ -101,7 +101,7 @@
},
"redeemed": "eingelöst",
"remove": "Entfernen",
- "removeNotSelf": "Als Admin / Moderator kannst du dich nicht selber löschen.",
+ "removeNotSelf": "Als Admin/Moderator kannst du dich nicht selber löschen.",
"remove_all": "alle Nutzer entfernen",
"save": "Speichern",
"status": "Status",
@@ -131,6 +131,16 @@
"text_false": " Die letzte Email wurde am {date} Uhr an das Mitglied ({email}) gesendet.",
"text_true": " Die Email wurde bestätigt."
},
+ "userRole": {
+ "notChangeYourSelf": "Als Admin/Moderator kannst du nicht selber deine Rolle ändern.",
+ "selectLabel": "Rolle:",
+ "selectRoles": {
+ "admin": "Administrator",
+ "user": "einfacher Nutzer"
+ },
+ "successfullyChangedTo": "Nutzer ist jetzt „{role}“.",
+ "tabTitle": "Nutzer-Rolle"
+ },
"user_deleted": "Nutzer ist gelöscht.",
"user_recovered": "Nutzer ist wiederhergestellt.",
"user_search": "Nutzer-Suche"
diff --git a/admin/src/locales/en.json b/admin/src/locales/en.json
index 0c8cc8c62..6d19b1732 100644
--- a/admin/src/locales/en.json
+++ b/admin/src/locales/en.json
@@ -101,7 +101,7 @@
},
"redeemed": "redeemed",
"remove": "Remove",
- "removeNotSelf": "As admin / moderator you cannot delete yourself.",
+ "removeNotSelf": "As an admin/moderator, you cannot delete yourself.",
"remove_all": "Remove all users",
"save": "Speichern",
"status": "Status",
@@ -131,6 +131,16 @@
"text_false": "The last email was sent to the member ({email}) on {date}.",
"text_true": "The email was confirmed."
},
+ "userRole": {
+ "notChangeYourSelf": "As an admin/moderator, you cannot change your own role.",
+ "selectLabel": "Role:",
+ "selectRoles": {
+ "admin": "administrator",
+ "user": "usual user"
+ },
+ "successfullyChangedTo": "User is now \"{role}\".",
+ "tabTitle": "User Role"
+ },
"user_deleted": "User is deleted.",
"user_recovered": "User is recovered.",
"user_search": "User search"
diff --git a/admin/src/pages/UserSearch.spec.js b/admin/src/pages/UserSearch.spec.js
index 7d8be648f..1020d2b00 100644
--- a/admin/src/pages/UserSearch.spec.js
+++ b/admin/src/pages/UserSearch.spec.js
@@ -199,14 +199,43 @@ describe('UserSearch', () => {
})
})
+ describe('change user role', () => {
+ const userId = 4
+
+ describe('to admin', () => {
+ it('updates user role to admin', async () => {
+ await wrapper
+ .findComponent({ name: 'SearchUserTable' })
+ .vm.$emit('updateIsAdmin', userId, new Date())
+ expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).isAdmin).toEqual(
+ expect.any(Date),
+ )
+ })
+ })
+
+ describe('to usual user', () => {
+ it('updates user role to usual user', async () => {
+ await wrapper
+ .findComponent({ name: 'SearchUserTable' })
+ .vm.$emit('updateIsAdmin', userId, null)
+ expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).isAdmin).toEqual(null)
+ })
+ })
+ })
+
describe('delete user', () => {
- const now = new Date()
- beforeEach(async () => {
- wrapper.findComponent({ name: 'SearchUserTable' }).vm.$emit('updateDeletedAt', 4, now)
+ const userId = 4
+ beforeEach(() => {
+ wrapper
+ .findComponent({ name: 'SearchUserTable' })
+ .vm.$emit('updateDeletedAt', userId, new Date())
})
it('marks the user as deleted', () => {
- expect(wrapper.vm.searchResult.find((obj) => obj.userId === 4).deletedAt).toEqual(now)
+ expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).deletedAt).toEqual(
+ expect.any(Date),
+ )
+ expect(wrapper.find('.test-deleted-icon').exists()).toBe(true)
})
it('toasts a success message', () => {
diff --git a/admin/src/pages/UserSearch.vue b/admin/src/pages/UserSearch.vue
index f1ca31d14..8eb1f9c63 100644
--- a/admin/src/pages/UserSearch.vue
+++ b/admin/src/pages/UserSearch.vue
@@ -42,6 +42,7 @@
type="PageUserSearch"
:items="searchResult"
:fields="fields"
+ @updateIsAdmin="updateIsAdmin"
@updateDeletedAt="updateDeletedAt"
/>
obj.userId === userId).isAdmin = isAdmin
+ },
updateDeletedAt(userId, deletedAt) {
this.searchResult.find((obj) => obj.userId === userId).deletedAt = deletedAt
this.toastSuccess(deletedAt ? this.$t('user_deleted') : this.$t('user_recovered'))
diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts
index 57ab98847..fc2b5342c 100644
--- a/backend/src/auth/RIGHTS.ts
+++ b/backend/src/auth/RIGHTS.ts
@@ -27,6 +27,9 @@ export enum RIGHTS {
GDT_BALANCE = 'GDT_BALANCE',
// Admin
SEARCH_USERS = 'SEARCH_USERS',
+ SET_USER_ROLE = 'SET_USER_ROLE',
+ DELETE_USER = 'DELETE_USER',
+ UNDELETE_USER = 'UNDELETE_USER',
ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION',
ADMIN_CREATE_CONTRIBUTIONS = 'ADMIN_CREATE_CONTRIBUTIONS',
ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
@@ -34,8 +37,6 @@ export enum RIGHTS {
LIST_UNCONFIRMED_CONTRIBUTIONS = 'LIST_UNCONFIRMED_CONTRIBUTIONS',
CONFIRM_CONTRIBUTION = 'CONFIRM_CONTRIBUTION',
SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL',
- DELETE_USER = 'DELETE_USER',
- UNDELETE_USER = 'UNDELETE_USER',
CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST',
LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN',
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
diff --git a/backend/src/graphql/model/UserAdmin.ts b/backend/src/graphql/model/UserAdmin.ts
index 8a1459c0f..cf3663e70 100644
--- a/backend/src/graphql/model/UserAdmin.ts
+++ b/backend/src/graphql/model/UserAdmin.ts
@@ -14,6 +14,7 @@ export class UserAdmin {
this.hasElopage = hasElopage
this.deletedAt = user.deletedAt
this.emailConfirmationSend = emailConfirmationSend
+ this.isAdmin = user.isAdmin
}
@Field(() => Number)
@@ -42,6 +43,9 @@ export class UserAdmin {
@Field(() => String, { nullable: true })
emailConfirmationSend?: string
+
+ @Field(() => Date, { nullable: true })
+ isAdmin: Date | null
}
@ObjectType()
diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts
index 2a973f1e8..404ed519e 100644
--- a/backend/src/graphql/resolver/AdminResolver.test.ts
+++ b/backend/src/graphql/resolver/AdminResolver.test.ts
@@ -13,6 +13,7 @@ import { peterLustig } from '@/seeds/users/peter-lustig'
import { stephenHawking } from '@/seeds/users/stephen-hawking'
import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
import {
+ setUserRole,
deleteUser,
unDeleteUser,
adminCreateContribution,
@@ -69,6 +70,161 @@ let user: User
let creation: Contribution | void
describe('AdminResolver', () => {
+ describe('set user role', () => {
+ describe('unauthenticated', () => {
+ it('returns an error', async () => {
+ await expect(
+ mutate({ mutation: setUserRole, variables: { userId: 1, isAdmin: true } }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+
+ describe('authenticated', () => {
+ describe('without admin rights', () => {
+ beforeAll(async () => {
+ user = await userFactory(testEnv, bibiBloxberg)
+ await query({
+ query: login,
+ variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
+ })
+ })
+
+ afterAll(async () => {
+ await cleanDB()
+ resetToken()
+ })
+
+ it('returns an error', async () => {
+ await expect(
+ mutate({ mutation: setUserRole, variables: { userId: user.id + 1, isAdmin: true } }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+
+ describe('with admin rights', () => {
+ beforeAll(async () => {
+ admin = await userFactory(testEnv, peterLustig)
+ await query({
+ query: login,
+ variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
+ })
+ })
+
+ afterAll(async () => {
+ await cleanDB()
+ resetToken()
+ })
+
+ describe('user to get a new role does not exist', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({ mutation: setUserRole, variables: { userId: admin.id + 1, isAdmin: true } }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError(`Could not find user with userId: ${admin.id + 1}`)],
+ }),
+ )
+ })
+ })
+
+ describe('change role with success', () => {
+ beforeAll(async () => {
+ user = await userFactory(testEnv, bibiBloxberg)
+ })
+
+ describe('user gets new role', () => {
+ describe('to admin', () => {
+ it('returns date string', async () => {
+ const result = await mutate({
+ mutation: setUserRole,
+ variables: { userId: user.id, isAdmin: true },
+ })
+ expect(result).toEqual(
+ expect.objectContaining({
+ data: {
+ setUserRole: expect.any(String),
+ },
+ }),
+ )
+ expect(new Date(result.data.setUserRole)).toEqual(expect.any(Date))
+ })
+ })
+
+ describe('to usual user', () => {
+ it('returns null', async () => {
+ await expect(
+ mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: false } }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ data: {
+ setUserRole: null,
+ },
+ }),
+ )
+ })
+ })
+ })
+ })
+
+ describe('change role with error', () => {
+ describe('is own role', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({ mutation: setUserRole, variables: { userId: admin.id, isAdmin: false } }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('Administrator can not change his own role!')],
+ }),
+ )
+ })
+ })
+
+ describe('user has already role to be set', () => {
+ describe('to admin', () => {
+ it('throws an error', async () => {
+ await mutate({
+ mutation: setUserRole,
+ variables: { userId: user.id, isAdmin: true },
+ })
+ await expect(
+ mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: true } }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('User is already admin!')],
+ }),
+ )
+ })
+ })
+
+ describe('to usual user', () => {
+ it('throws an error', async () => {
+ await mutate({
+ mutation: setUserRole,
+ variables: { userId: user.id, isAdmin: false },
+ })
+ await expect(
+ mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: false } }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('User is already a usual user!')],
+ }),
+ )
+ })
+ })
+ })
+ })
+ })
+ })
+ })
+
describe('delete user', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts
index 62582e16f..825afae94 100644
--- a/backend/src/graphql/resolver/AdminResolver.ts
+++ b/backend/src/graphql/resolver/AdminResolver.ts
@@ -73,7 +73,15 @@ export class AdminResolver {
}
}
- const userFields = ['id', 'firstName', 'lastName', 'email', 'emailChecked', 'deletedAt']
+ const userFields = [
+ 'id',
+ 'firstName',
+ 'lastName',
+ 'email',
+ 'emailChecked',
+ 'deletedAt',
+ 'isAdmin',
+ ]
const [users, count] = await userRepository.findBySearchCriteriaPagedFiltered(
userFields.map((fieldName) => {
return 'user.' + fieldName
@@ -133,6 +141,48 @@ export class AdminResolver {
}
}
+ @Authorized([RIGHTS.SET_USER_ROLE])
+ @Mutation(() => Date, { nullable: true })
+ async setUserRole(
+ @Arg('userId', () => Int)
+ userId: number,
+ @Arg('isAdmin', () => Boolean)
+ isAdmin: boolean,
+ @Ctx()
+ context: Context,
+ ): Promise {
+ const user = await dbUser.findOne({ id: userId })
+ // user exists ?
+ if (!user) {
+ throw new Error(`Could not find user with userId: ${userId}`)
+ }
+ // administrator user changes own role?
+ const moderatorUser = getUser(context)
+ if (moderatorUser.id === userId) {
+ throw new Error('Administrator can not change his own role!')
+ }
+ // change isAdmin
+ switch (user.isAdmin) {
+ case null:
+ if (isAdmin === true) {
+ user.isAdmin = new Date()
+ } else {
+ throw new Error('User is already a usual user!')
+ }
+ break
+ default:
+ if (isAdmin === false) {
+ user.isAdmin = null
+ } else {
+ throw new Error('User is already admin!')
+ }
+ break
+ }
+ await user.save()
+ const newUser = await dbUser.findOne({ id: userId })
+ return newUser ? newUser.isAdmin : null
+ }
+
@Authorized([RIGHTS.DELETE_USER])
@Mutation(() => Date, { nullable: true })
async deleteUser(
diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts
index 7becae274..f2edf0821 100644
--- a/backend/src/seeds/graphql/mutations.ts
+++ b/backend/src/seeds/graphql/mutations.ts
@@ -98,6 +98,12 @@ export const confirmContribution = gql`
}
`
+export const setUserRole = gql`
+ mutation ($userId: Int!, $isAdmin: Boolean!) {
+ setUserRole(userId: $userId, isAdmin: $isAdmin)
+ }
+`
+
export const deleteUser = gql`
mutation ($userId: Int!) {
deleteUser(userId: $userId)
diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts
index 16818446e..531aebf0f 100644
--- a/backend/src/seeds/graphql/queries.ts
+++ b/backend/src/seeds/graphql/queries.ts
@@ -110,6 +110,7 @@ export const searchUsers = gql`
hasElopage
emailConfirmationSend
deletedAt
+ isAdmin
}
}
}