Merge remote-tracking branch 'origin/master' into 3110-featuredlt-connector-gradido-transaktionen-auf-tangle-senden-und-empfangen

This commit is contained in:
Claus-Peter Huebner 2023-07-27 17:32:55 +02:00
commit ba026ae335
33 changed files with 651 additions and 123 deletions

37
.github/dependabot_backend.yml vendored Normal file
View File

@ -0,0 +1,37 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/backend"
rebase-strategy: "disabled"
schedule:
interval: weekly
day: "saturday"
timezone: "Europe/Berlin"
time: "03:00"
labels:
- "devops"
- "service:backend"
- package-ecosystem: docker
directory: "/backend"
rebase-strategy: "disabled"
schedule:
interval: weekly
day: "saturday"
timezone: "Europe/Berlin"
time: "03:00"
labels:
- "devops"
- "service:docker"
- package-ecosystem: "github-actions"
directory: "/"
rebase-strategy: "disabled"
schedule:
interval: weekly
day: "saturday"
timezone: "Europe/Berlin"
time: "03:00"
labels:
- "devops"

View File

@ -4,8 +4,24 @@ 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).
#### [1.23.0](https://github.com/gradido/gradido/compare/1.22.3...1.23.0)
- refactor(frontend): add contribution by link information to locales [`#3144`](https://github.com/gradido/gradido/pull/3144)
- fix(admin): user role in admin interface [`#3153`](https://github.com/gradido/gradido/pull/3153)
- feat(backend): 3030 feature role administration backend [`#3074`](https://github.com/gradido/gradido/pull/3074)
- feat(other): iota-tangle-connector sending transaction [`#3132`](https://github.com/gradido/gradido/pull/3132)
- feat(other): proper reporting for failing end-to-end tests [`#3096`](https://github.com/gradido/gradido/pull/3096)
- fix(other): add missing volume for dlt-connector dev docker [`#3134`](https://github.com/gradido/gradido/pull/3134)
- fix(backend): semaphore parallel redeemTransactionLink test [`#3133`](https://github.com/gradido/gradido/pull/3133)
- feat(other): iota-tangle-connector mit Hello World Message als separates Modul [`#3118`](https://github.com/gradido/gradido/pull/3118)
- refactor(database): fix database public key lengths [`#3026`](https://github.com/gradido/gradido/pull/3026)
- refactor(dht): eslint dht import [`#3044`](https://github.com/gradido/gradido/pull/3044)
#### [1.22.3](https://github.com/gradido/gradido/compare/1.22.2...1.22.3)
> 6 July 2023
- chore(release): v1.22.3 [`#3131`](https://github.com/gradido/gradido/pull/3131)
- fix(backend): corrected email-link [`#3129`](https://github.com/gradido/gradido/pull/3129)
#### [1.22.2](https://github.com/gradido/gradido/compare/1.22.1...1.22.2)

View File

@ -3,7 +3,7 @@
"description": "Administraion Interface for Gradido",
"main": "index.js",
"author": "Moriz Wahl",
"version": "1.22.3",
"version": "1.23.0",
"license": "Apache-2.0",
"private": false,
"scripts": {

View File

@ -21,6 +21,7 @@ const mocks = {
moderator: {
id: 0,
name: 'test moderator',
roles: ['ADMIN'],
},
},
},
@ -45,7 +46,7 @@ describe('ChangeUserRoleFormular', () => {
propsData = {
item: {
userId: 1,
isAdmin: null,
roles: [],
},
}
wrapper = Wrapper()
@ -61,7 +62,7 @@ describe('ChangeUserRoleFormular', () => {
propsData = {
item: {
userId: 0,
isAdmin: null,
roles: ['ADMIN'],
},
}
wrapper = Wrapper()
@ -88,7 +89,7 @@ describe('ChangeUserRoleFormular', () => {
propsData = {
item: {
userId: 1,
isAdmin: null,
roles: [],
},
}
wrapper = Wrapper()
@ -120,13 +121,13 @@ describe('ChangeUserRoleFormular', () => {
beforeEach(() => {
apolloMutateMock.mockResolvedValue({
data: {
setUserRole: new Date(),
setUserRole: 'ADMIN',
},
})
propsData = {
item: {
userId: 1,
isAdmin: null,
roles: ['USER'],
},
}
wrapper = Wrapper()
@ -134,7 +135,7 @@ describe('ChangeUserRoleFormular', () => {
})
it('has selected option set to "usual user"', () => {
expect(wrapper.find('select.role-select').element.value).toBe('user')
expect(wrapper.find('select.role-select').element.value).toBe('USER')
})
describe('change select to', () => {
@ -149,7 +150,7 @@ describe('ChangeUserRoleFormular', () => {
})
})
describe('new role', () => {
describe('new role "MODERATOR"', () => {
beforeEach(() => {
rolesToSelect.at(1).setSelected()
})
@ -181,19 +182,267 @@ describe('ChangeUserRoleFormular', () => {
mutation: setUserRole,
variables: {
userId: 1,
isAdmin: true,
role: 'MODERATOR',
},
}),
)
})
it('emits "updateIsAdmin"', () => {
expect(wrapper.emitted('updateIsAdmin')).toEqual(
it('emits "updateRoles" with role moderator', () => {
expect(wrapper.emitted('updateRoles')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
isAdmin: expect.any(Date),
roles: ['MODERATOR'],
},
]),
]),
)
})
it('toasts success message', () => {
expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo')
})
})
describe('confirm role change with error', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
})
})
describe('new role "ADMIN"', () => {
beforeEach(() => {
rolesToSelect.at(2).setSelected()
})
it('has "change_user_role" button enabled', () => {
expect(wrapper.find('button.btn.btn-danger').exists()).toBe(true)
expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe(
false,
)
})
describe('clicking the "change_user_role" button', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
spy.mockImplementation(() => Promise.resolve(true))
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('calls the modal', () => {
expect(wrapper.emitted('showModal'))
expect(spy).toHaveBeenCalled()
})
describe('confirm role change with success', () => {
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: setUserRole,
variables: {
userId: 1,
role: 'ADMIN',
},
}),
)
})
it('emits "updateRoles" with role moderator', () => {
expect(wrapper.emitted('updateRoles')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
roles: ['ADMIN'],
},
]),
]),
)
})
it('toasts success message', () => {
expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo')
})
})
describe('confirm role change with error', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
})
})
})
})
describe('user has role "moderator"', () => {
beforeEach(() => {
apolloMutateMock.mockResolvedValue({
data: {
setUserRole: null,
},
})
propsData = {
item: {
userId: 1,
roles: ['MODERATOR'],
},
}
wrapper = Wrapper()
rolesToSelect = wrapper.find('select.role-select').findAll('option')
})
it('has selected option set to "MODERATOR"', () => {
expect(wrapper.find('select.role-select').element.value).toBe('MODERATOR')
})
describe('change select to', () => {
describe('same role', () => {
it('has "change_user_role" button disabled', () => {
expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe(true)
})
it('does not call the API', () => {
rolesToSelect.at(1).setSelected()
expect(apolloMutateMock).not.toHaveBeenCalled()
})
})
describe('new role "USER"', () => {
beforeEach(() => {
rolesToSelect.at(0).setSelected()
})
it('has "change_user_role" button enabled', () => {
expect(wrapper.find('button.btn.btn-danger').exists()).toBe(true)
expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe(
false,
)
})
describe('clicking the "change_user_role" button', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
spy.mockImplementation(() => Promise.resolve(true))
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('calls the modal', () => {
expect(wrapper.emitted('showModal'))
expect(spy).toHaveBeenCalled()
})
describe('confirm role change with success', () => {
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: setUserRole,
variables: {
userId: 1,
role: 'USER',
},
}),
)
})
it('emits "updateRoles"', () => {
expect(wrapper.emitted('updateRoles')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
roles: [],
},
]),
]),
)
})
it('toasts success message', () => {
expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo')
})
})
describe('confirm role change with error', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
})
})
describe('new role "ADMIN"', () => {
beforeEach(() => {
rolesToSelect.at(2).setSelected()
})
it('has "change_user_role" button enabled', () => {
expect(wrapper.find('button.btn.btn-danger').exists()).toBe(true)
expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe(
false,
)
})
describe('clicking the "change_user_role" button', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
spy.mockImplementation(() => Promise.resolve(true))
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('calls the modal', () => {
expect(wrapper.emitted('showModal'))
expect(spy).toHaveBeenCalled()
})
describe('confirm role change with success', () => {
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: setUserRole,
variables: {
userId: 1,
role: 'ADMIN',
},
}),
)
})
it('emits "updateRoles"', () => {
expect(wrapper.emitted('updateRoles')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
roles: ['ADMIN'],
},
]),
]),
@ -232,7 +481,7 @@ describe('ChangeUserRoleFormular', () => {
propsData = {
item: {
userId: 1,
isAdmin: new Date(),
roles: ['ADMIN'],
},
}
wrapper = Wrapper()
@ -240,7 +489,7 @@ describe('ChangeUserRoleFormular', () => {
})
it('has selected option set to "admin"', () => {
expect(wrapper.find('select.role-select').element.value).toBe('admin')
expect(wrapper.find('select.role-select').element.value).toBe('ADMIN')
})
describe('change select to', () => {
@ -251,11 +500,12 @@ describe('ChangeUserRoleFormular', () => {
it('does not call the API', () => {
rolesToSelect.at(1).setSelected()
// TODO: Fix this
expect(apolloMutateMock).not.toHaveBeenCalled()
})
})
describe('new role', () => {
describe('new role "USER"', () => {
beforeEach(() => {
rolesToSelect.at(0).setSelected()
})
@ -287,19 +537,90 @@ describe('ChangeUserRoleFormular', () => {
mutation: setUserRole,
variables: {
userId: 1,
isAdmin: false,
role: 'USER',
},
}),
)
})
it('emits "updateIsAdmin"', () => {
expect(wrapper.emitted('updateIsAdmin')).toEqual(
it('emits "updateRoles"', () => {
expect(wrapper.emitted('updateRoles')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
isAdmin: null,
roles: [],
},
]),
]),
)
})
it('toasts success message', () => {
expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo')
})
})
describe('confirm role change with error', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
})
})
describe('new role "MODERATOR"', () => {
beforeEach(() => {
rolesToSelect.at(1).setSelected()
})
it('has "change_user_role" button enabled', () => {
expect(wrapper.find('button.btn.btn-danger').exists()).toBe(true)
expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe(
false,
)
})
describe('clicking the "change_user_role" button', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
spy.mockImplementation(() => Promise.resolve(true))
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('calls the modal', () => {
expect(wrapper.emitted('showModal'))
expect(spy).toHaveBeenCalled()
})
describe('confirm role change with success', () => {
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: setUserRole,
variables: {
userId: 1,
role: 'MODERATOR',
},
}),
)
})
it('emits "updateRoles"', () => {
expect(wrapper.emitted('updateRoles')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
roles: ['MODERATOR'],
},
]),
]),
@ -328,5 +649,23 @@ describe('ChangeUserRoleFormular', () => {
})
})
})
describe('authenticated user is MODERATOR', () => {
beforeEach(() => {
mocks.$store.state.moderator.roles = ['MODERATOR']
})
it('displays text with role', () => {
expect(wrapper.text()).toBe('userRole.selectRoles.admin')
})
it('has no role select', () => {
expect(wrapper.find('select.role-select').exists()).toBe(false)
})
it('has no button', () => {
expect(wrapper.find('button.btn.btn-dange').exists()).toBe(false)
})
})
})
})

View File

@ -1,7 +1,10 @@
<template>
<div class="change-user-role-formular">
<div class="shadow p-3 mb-5 bg-white rounded">
<div v-if="item.userId === $store.state.moderator.id" class="m-3 mb-4">
<div v-if="!$store.state.moderator.roles.includes('ADMIN')" class="m-3 mb-4">
{{ roles.find((role) => role.value === currentRole).text }}
</div>
<div v-else-if="item.userId === $store.state.moderator.id" class="m-3 mb-4">
{{ $t('userRole.notChangeYourSelf') }}
</div>
<div v-else class="m-3">
@ -25,8 +28,9 @@
import { setUserRole } from '../graphql/setUserRole'
const rolesValues = {
admin: 'admin',
user: 'user',
ADMIN: 'ADMIN',
MODERATOR: 'MODERATOR',
USER: 'USER',
}
export default {
@ -39,23 +43,30 @@ export default {
},
data() {
return {
currentRole: this.item.isAdmin ? rolesValues.admin : rolesValues.user,
roleSelected: this.item.isAdmin ? rolesValues.admin : rolesValues.user,
currentRole: this.getCurrentRole(),
roleSelected: this.getCurrentRole(),
roles: [
{ value: rolesValues.user, text: this.$t('userRole.selectRoles.user') },
{ value: rolesValues.admin, text: this.$t('userRole.selectRoles.admin') },
{ value: rolesValues.USER, text: this.$t('userRole.selectRoles.user') },
{ value: rolesValues.MODERATOR, text: this.$t('userRole.selectRoles.moderator') },
{ value: rolesValues.ADMIN, text: this.$t('userRole.selectRoles.admin') },
],
}
},
methods: {
getCurrentRole() {
if (this.item.roles.length) return rolesValues[this.item.roles[0]]
return rolesValues.USER
},
showModal() {
this.$bvModal
.msgBoxConfirm(
this.$t('overlay.changeUserRole.question', {
username: `${this.item.firstName} ${this.item.lastName}`,
newRole:
this.roleSelected === 'admin'
this.roleSelected === rolesValues.ADMIN
? this.$t('userRole.selectRoles.admin')
: this.roleSelected === rolesValues.MODERATOR
? this.$t('userRole.selectRoles.moderator')
: this.$t('userRole.selectRoles.user'),
}),
{
@ -77,25 +88,27 @@ export default {
})
},
setUserRole(newRole, oldRole) {
const role = this.roles.find((role) => {
return role.value === newRole
})
const roleText = role.text
const roleValue = role.value
this.$apollo
.mutate({
mutation: setUserRole,
variables: {
userId: this.item.userId,
isAdmin: newRole === rolesValues.admin,
role: role.value,
},
})
.then((result) => {
this.$emit('updateIsAdmin', {
this.$emit('updateRoles', {
userId: this.item.userId,
isAdmin: result.data.setUserRole,
roles: roleValue === 'USER' ? [] : [roleValue],
})
this.toastSuccess(
this.$t('userRole.successfullyChangedTo', {
role:
result.data.setUserRole !== null
? this.$t('userRole.selectRoles.admin')
: this.$t('userRole.selectRoles.user'),
role: roleText,
}),
)
})

View File

@ -15,6 +15,7 @@ const propsData = {
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
emailChecked: true,
roles: [],
},
{
userId: 2,
@ -23,6 +24,7 @@ const propsData = {
email: 'benjamin@bluemchen.de',
creation: [1000, 1000, 1000],
emailChecked: true,
roles: [],
},
{
userId: 3,
@ -31,6 +33,7 @@ const propsData = {
email: 'peter@lustig.de',
creation: [0, 0, 0],
emailChecked: true,
roles: ['ADMIN'],
},
{
userId: 4,
@ -39,6 +42,7 @@ const propsData = {
email: 'new@user.ch',
creation: [1000, 1000, 1000],
emailChecked: false,
roles: [],
},
],
fields: [
@ -68,6 +72,7 @@ const mocks = {
moderator: {
id: 0,
name: 'test moderator',
roles: ['ADMIN'],
},
},
},
@ -96,14 +101,14 @@ describe('SearchUserTable', () => {
describe('isAdmin', () => {
beforeEach(async () => {
await wrapper.find('div.change-user-role-formular').vm.$emit('updateIsAdmin', {
await wrapper.find('div.change-user-role-formular').vm.$emit('updateRoles', {
userId: 1,
isAdmin: new Date(),
roles: ['ADMIN'],
})
})
it('emits updateIsAdmin', () => {
expect(wrapper.emitted('updateIsAdmin')).toEqual([[1, expect.any(Date)]])
expect(wrapper.emitted('updateRoles')).toEqual([[1, ['ADMIN']]])
})
})

View File

@ -79,9 +79,9 @@
<transaction-link-list v-if="!row.item.deletedAt" :userId="row.item.userId" />
</b-tab>
<b-tab :title="$t('userRole.tabTitle')">
<change-user-role-formular :item="row.item" @updateIsAdmin="updateIsAdmin" />
<change-user-role-formular :item="row.item" @updateRoles="updateRoles" />
</b-tab>
<b-tab :title="$t('delete_user')">
<b-tab v-if="$store.state.moderator.roles.includes('ADMIN')" :title="$t('delete_user')">
<deleted-user-formular :item="row.item" @updateDeletedAt="updateDeletedAt" />
</b-tab>
</b-tabs>
@ -127,8 +127,8 @@ export default {
updateUserData(rowItem, newCreation) {
rowItem.creation = newCreation
},
updateIsAdmin({ userId, isAdmin }) {
this.$emit('updateIsAdmin', userId, isAdmin)
updateRoles({ userId, roles }) {
this.$emit('updateRoles', userId, roles)
},
updateDeletedAt({ userId, deletedAt }) {
this.$emit('updateDeletedAt', userId, deletedAt)

View File

@ -0,0 +1,47 @@
import { mount } from '@vue/test-utils'
import UserQuery from './UserQuery'
const localVue = global.localVue
const propsData = {
userId: 42,
}
const mocks = {
$t: jest.fn((t) => t),
}
describe('TransactionLinkList', () => {
let wrapper
const Wrapper = () => {
return mount(UserQuery, { mocks, localVue, propsData })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has div .input-group', () => {
expect(wrapper.find('div .input-group').exists()).toBe(true)
})
it('has .test-input-criteria', () => {
expect(wrapper.find('input.test-input-criteria').exists()).toBe(true)
})
describe('set value', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.find('input.test-input-criteria').setValue('Test2')
})
it('emits input', () => {
expect(wrapper.emitted('input')).toBeTruthy()
})
it('emits input with value "Test2"', () => {
expect(wrapper.emitted('input')).toEqual([['Test2']])
})
})
})
})

View File

@ -26,7 +26,7 @@ export const searchUsers = gql`
hasElopage
emailConfirmationSend
deletedAt
isAdmin
roles
}
}
}

View File

@ -1,7 +1,7 @@
import gql from 'graphql-tag'
export const setUserRole = gql`
mutation ($userId: Int!, $isAdmin: Boolean!) {
setUserRole(userId: $userId, isAdmin: $isAdmin)
mutation ($userId: Int!, $role: RoleNames!) {
setUserRole(userId: $userId, role: $role)
}
`

View File

@ -5,7 +5,7 @@ export const verifyLogin = gql`
verifyLogin {
firstName
lastName
isAdmin
roles
id
language
}

View File

@ -209,6 +209,7 @@
"selectLabel": "Rolle:",
"selectRoles": {
"admin": "Administrator",
"moderator": "Moderator",
"user": "einfacher Nutzer"
},
"successfullyChangedTo": "Nutzer ist jetzt „{role}“.",

View File

@ -209,6 +209,7 @@
"selectLabel": "Role:",
"selectRoles": {
"admin": "administrator",
"moderator": "moderator",
"user": "usual user"
},
"successfullyChangedTo": "User is now \"{role}\".",

View File

@ -28,7 +28,7 @@ const mocks = {
moderator: {
firstName: 'Peter',
lastName: 'Lustig',
isAdmin: '2022-08-30T07:41:31.000Z',
roles: ['ADMIN'],
id: 263,
language: 'de',
},

View File

@ -16,6 +16,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
email: 'new@user.ch',
creation: [1000, 1000, 1000],
emailChecked: false,
roles: [],
deletedAt: null,
},
{
@ -24,6 +25,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
lastName: 'Lustig',
email: 'peter@lustig.de',
creation: [0, 0, 0],
roles: ['ADMIN'],
emailChecked: true,
deletedAt: null,
},
@ -33,6 +35,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [1000, 1000, 1000],
roles: [],
emailChecked: true,
deletedAt: new Date(),
},
@ -42,6 +45,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
roles: [],
emailChecked: true,
deletedAt: null,
},
@ -212,10 +216,10 @@ describe('UserSearch', () => {
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),
)
.vm.$emit('updateRoles', userId, ['ADMIN'])
expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).roles).toEqual([
'ADMIN',
])
})
})
@ -223,8 +227,8 @@ describe('UserSearch', () => {
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)
.vm.$emit('updateRoles', userId, [])
expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).roles).toEqual([])
})
})
})

View File

@ -28,7 +28,7 @@
type="PageUserSearch"
:items="searchResult"
:fields="fields"
@updateIsAdmin="updateIsAdmin"
@updateRoles="updateRoles"
@updateDeletedAt="updateDeletedAt"
/>
<b-pagination
@ -101,8 +101,8 @@ export default {
this.toastError(error.message)
})
},
updateIsAdmin(userId, isAdmin) {
this.searchResult.find((obj) => obj.userId === userId).isAdmin = isAdmin
updateRoles(userId, roles) {
this.searchResult.find((obj) => obj.userId === userId).roles = roles
},
updateDeletedAt(userId, deletedAt) {
this.searchResult.find((obj) => obj.userId === userId).deletedAt = deletedAt

View File

@ -13,7 +13,7 @@ const addNavigationGuards = (router, store, apollo, i18n) => {
})
.then((result) => {
const moderator = result.data.verifyLogin
if (moderator.isAdmin) {
if (moderator.roles?.length) {
i18n.locale = moderator.language
store.commit('moderator', moderator)
next({ path: '/' })
@ -35,7 +35,7 @@ const addNavigationGuards = (router, store, apollo, i18n) => {
!CONFIG.DEBUG_DISABLE_AUTH && // we did not disabled the auth module for debug purposes
(!store.state.token || // we do not have a token
!store.state.moderator || // no moderator set in store
!store.state.moderator.isAdmin) && // user is no admin
!store.state.moderator.roles.length) && // user is no admin
to.path !== '/not-found' && // we are not on `not-found`
to.path !== '/logout' // we are not on `logout`
) {

View File

@ -5,7 +5,7 @@ const storeCommitMock = jest.fn()
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
verifyLogin: {
isAdmin: true,
roles: ['ADMIN'],
language: 'de',
},
},
@ -52,7 +52,10 @@ describe('navigation guards', () => {
})
it('commits the moderator to the store', () => {
expect(storeCommitMock).toBeCalledWith('moderator', { isAdmin: true, language: 'de' })
expect(storeCommitMock).toBeCalledWith('moderator', {
roles: ['ADMIN'],
language: 'de',
})
})
it('redirects to /', () => {
@ -60,12 +63,48 @@ describe('navigation guards', () => {
})
})
describe('with valid token and not as admin', () => {
beforeEach(() => {
describe('with valid token and as moderator', () => {
beforeEach(async () => {
jest.clearAllMocks()
apolloQueryMock.mockResolvedValue({
data: {
verifyLogin: {
isAdmin: false,
roles: ['MODERATOR'],
language: 'de',
},
},
})
await navGuard({ path: '/authenticate', query: { token: 'valid-token' } }, {}, next)
})
it('commits the token to the store', () => {
expect(storeCommitMock).toBeCalledWith('token', 'valid-token')
})
it.skip('sets the locale', () => {
expect(i18nLocaleMock).toBeCalledWith('de')
})
it('commits the moderator to the store', () => {
expect(storeCommitMock).toBeCalledWith('moderator', {
roles: ['MODERATOR'],
language: 'de',
})
})
it('redirects to /', () => {
expect(next).toBeCalledWith({ path: '/' })
})
})
describe('with valid token and no roles', () => {
beforeEach(() => {
jest.clearAllMocks()
apolloQueryMock.mockResolvedValue({
data: {
verifyLogin: {
roles: [],
language: 'de',
},
},
})
@ -77,7 +116,7 @@ describe('navigation guards', () => {
})
it('does not commit the moderator to the store', () => {
expect(storeCommitMock).not.toBeCalledWith('moderator', { isAdmin: false })
expect(storeCommitMock).not.toBeCalledWith('moderator')
})
it('redirects to /not-found', async () => {
@ -128,15 +167,22 @@ describe('navigation guards', () => {
expect(next).toBeCalledWith({ path: '/not-found' })
})
it('redirects to not found with token in store and not moderator', () => {
it('redirects to not found with token in store and not admin or moderator', () => {
store.state.token = 'valid token'
navGuard({ path: '/' }, {}, next)
expect(next).toBeCalledWith({ path: '/not-found' })
})
it('does not redirect with token in store and as admin', () => {
store.state.token = 'valid token'
store.state.moderator = { roles: ['ADMIN'] }
navGuard({ path: '/' }, {}, next)
expect(next).toBeCalledWith()
})
it('does not redirect with token in store and as moderator', () => {
store.state.token = 'valid token'
store.state.moderator = { isAdmin: true }
store.state.moderator = { roles: ['MODERATOR'] }
navGuard({ path: '/' }, {}, next)
expect(next).toBeCalledWith()
})

View File

@ -1,6 +1,6 @@
{
"name": "gradido-backend",
"version": "1.22.3",
"version": "1.23.0",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend",

View File

@ -655,9 +655,7 @@ export class UserResolver {
const clientTimezoneOffset = getClientTimezoneOffset(context)
const userFields = ['id', 'firstName', 'lastName', 'emailId', 'emailContact', 'deletedAt']
const [users, count] = await findUsers(
userFields.map((fieldName) => {
return 'user.' + fieldName
}),
userFields,
query,
filters ?? null,
currentPage,

View File

@ -1,10 +1,24 @@
import { getConnection, Brackets, IsNull, Not } from '@dbTools/typeorm'
import { IsNull, Not, Like } from '@dbTools/typeorm'
import { User as DbUser } from '@entity/User'
import { SearchUsersFilters } from '@arg/SearchUsersFilters'
import { Order } from '@enum/Order'
import { LogError } from '@/server/LogError'
function likeQuery(searchCriteria: string) {
return Like(`%${searchCriteria}%`)
}
function emailCheckedQuery(filters: SearchUsersFilters) {
return filters.byActivated ?? undefined
}
function deletedAtQuery(filters: SearchUsersFilters | null) {
return filters?.byDeleted !== undefined && filters?.byDeleted !== null
? filters.byDeleted
? Not(IsNull())
: IsNull()
: undefined
}
export const findUsers = async (
select: string[],
@ -14,44 +28,51 @@ export const findUsers = async (
pageSize: number,
order = Order.ASC,
): Promise<[DbUser[], number]> => {
const queryRunner = getConnection().createQueryRunner()
try {
await queryRunner.connect()
const query = queryRunner.manager
.createQueryBuilder(DbUser, 'user')
.select(select)
.withDeleted()
.leftJoinAndSelect('user.emailContact', 'emailContact')
.where(
new Brackets((qb) => {
qb.where(
'user.firstName like :name or user.lastName like :lastName or emailContact.email like :email',
{
name: `%${searchCriteria}%`,
lastName: `%${searchCriteria}%`,
email: `%${searchCriteria}%`,
},
)
}),
)
if (filters) {
if (filters.byActivated !== null) {
query.andWhere('emailContact.emailChecked = :value', { value: filters.byActivated })
}
if (filters.byDeleted !== null) {
query.andWhere({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() })
}
}
return await query
.orderBy({ 'user.id': order })
.take(pageSize)
.skip((currentPage - 1) * pageSize)
.getManyAndCount()
} catch (err) {
throw new LogError('Unable to search users', err)
} finally {
await queryRunner.release()
const where = [
{
firstName: likeQuery(searchCriteria),
deletedAt: deletedAtQuery(filters),
emailContact: filters
? {
emailChecked: emailCheckedQuery(filters),
}
: undefined,
},
{
lastName: likeQuery(searchCriteria),
deletedAt: deletedAtQuery(filters),
emailContact: filters
? {
emailChecked: emailCheckedQuery(filters),
}
: undefined,
},
{
emailContact: {
// ...(filters ?? emailChecked: filters.byActivated)
emailChecked: filters ? emailCheckedQuery(filters) : undefined,
email: likeQuery(searchCriteria),
},
deletedAt: deletedAtQuery(filters),
},
]
const selectFind = Object.fromEntries(select.map((item) => [item, true]))
const relations = ['emailContact', 'userRoles']
const orderFind = {
id: order,
}
const take = pageSize
const skip = (currentPage - 1) * pageSize
const withDeleted = true
const [users, count] = await DbUser.findAndCount({
where,
withDeleted,
select: selectFind,
relations,
order: orderFind,
take,
skip,
})
return [users, count]
}

View File

@ -16,7 +16,7 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis
await queryFn(`
INSERT INTO user_roles
(user_id, role, created_at, updated_at)
SELECT u.id, 'admin', u.is_admin, null
SELECT u.id, 'ADMIN', u.is_admin, null
FROM users u
WHERE u.is_admin IS NOT NULL;`)
@ -31,7 +31,7 @@ export async function downgrade(queryFn: (query: string, values?: any[]) => Prom
)
// reconstruct the previous is_admin back from user_roles to users table
const roles = await queryFn(
`SELECT r.user_id, r.role, r.created_at FROM user_roles as r WHERE r.role = "admin"`,
`SELECT r.user_id, r.role, r.created_at FROM user_roles as r WHERE r.role = "ADMIN"`,
)
for (const id in roles) {
const role = roles[id]

View File

@ -1,6 +1,6 @@
{
"name": "gradido-database",
"version": "1.22.3",
"version": "1.23.0",
"description": "Gradido Database Tool to execute database migrations",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/database",

View File

@ -1,6 +1,6 @@
{
"name": "gradido-dht-node",
"version": "1.22.3",
"version": "1.23.0",
"description": "Gradido dht-node module",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/",

View File

@ -1,6 +1,6 @@
{
"name": "gradido-federation",
"version": "1.22.3",
"version": "1.23.0",
"description": "Gradido federation module providing Gradido-Hub-Federation and versioned API for inter community communication",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/federation",

View File

@ -1,6 +1,6 @@
{
"name": "bootstrap-vue-gradido-wallet",
"version": "1.22.3",
"version": "1.23.0",
"private": true,
"scripts": {
"start": "node run/server.js",

View File

@ -49,7 +49,7 @@
</b-nav-item>
<b-nav-item
class="mb-3 text-light"
v-if="$store.state.roles.length > 0"
v-if="$store.state.roles && $store.state.roles.length > 0"
@click="$emit('admin')"
active-class="activeRoute"
>

View File

@ -29,7 +29,7 @@
"myContributions": "Meine Beiträge",
"noOpenContributionLinkText": "Zur Zeit gibt es keine per Link erzeugte Schöpfungen.",
"openContributionLinks": "Per Link erzeugte Schöpfungen",
"openContributionLinkText": "Die Gemeinschaft „{name}“ unterstützt aktuell {count} per Link erzeugte Schöpfungen:",
"openContributionLinkText": "Für Startguthaben oder ähnliche Zwecke kann die Gemeinschaft so genannte Schöpfungs-Links erstellen. Sie lösen automatische Schöpfungen aus, die dem Benutzer gut geschrieben werden.\nDie Gemeinschaft „{name}“ unterstützt aktuell {count} per Link erzeugte Schöpfungen:",
"startNewsButton": "Benutzernamen eintragen",
"submitContribution": "Schreiben"
},

View File

@ -29,7 +29,7 @@
"myContributions": "My contributions",
"noOpenContributionLinkText": "Currently there are no link-generated creations.",
"openContributionLinks": "Creations generated by link",
"openContributionLinkText": "The \"{name}\" community currently supports {count} link-generated creations:",
"openContributionLinkText": "For starting credits or similar purposes, the community can create so-called creation links. They trigger automatic creations that are credited to the user.\nThe \"{name}\" community currently supports {count} link-generated creations:",
"startNewsButton": "Enter username",
"submitContribution": "Contribute"
},

View File

@ -28,7 +28,7 @@
"myContributions": "Mis contribuciones al bien común",
"noOpenContributionLinkText": "Actualmente no hay creaciones generadas por enlaces.",
"openContributionLinks": "Creaciones generadas por enlace",
"openContributionLinkText": "La comunidad \"{name}\" admite actualmente {count} creaciones generadas por enlaces:",
"openContributionLinkText": "Para créditos iniciales o fines similares, la comunidad puede crear los llamados enlaces de creación. Éstos activan creaciones automáticas que se acreditan al usuario.\nLa comunidad \"{name}\" admite actualmente {count} creaciones generadas por enlaces:",
"other-communities": "Otras comunidades",
"startNewsButton": "Introducir nombre de usuario",
"statistic": "Estadísticas",

View File

@ -29,7 +29,7 @@
"myContributions": "Mes contributions",
"noOpenContributionLinkText": "Actuellement, il n'y a pas de créations générées par lien.",
"openContributionLinks": "Créations générées par lien",
"openContributionLinkText": "La communauté \"{name}\" soutient actuellement {count} créations générées par lien:",
"openContributionLinkText": "Pour les crédits de départ ou à des fins similaires, la communauté peut créer des \"liens de création\". Ils déclenchent des créations automatiques qui sont créditées à l'utilisateur.\nLa communauté \"{name}\" soutient actuellement {count} créations générées par lien:",
"startNewsButton": "Saisir nom d'utilisateur",
"submitContribution": "Contribuer"
},

View File

@ -28,7 +28,7 @@
"myContributions": "Mijn bijdragen voor het algemeen belang",
"noOpenContributionLinkText": "Er zijn momenteel geen link-gegenereerde creaties.",
"openContributionLinks": "Creaties gegenereerd door link",
"openContributionLinkText": "De community \"{name}\" ondersteunt momenteel {count} link-gegenereerde creaties:",
"openContributionLinkText": "Voor startcredits of soortgelijke doeleinden kan de community zogenaamde creatielinks maken. Deze activeren automatische creaties die worden gecrediteerd aan de gebruiker.\nDe community \"{name}\" ondersteunt momenteel {count} link-gegenereerde creaties:",
"other-communities": "Verdere gemeenschappen",
"startNewsButton": "Gebruikersnaam invoeren",
"statistic": "Statistieken",

View File

@ -1,6 +1,6 @@
{
"name": "gradido",
"version": "1.22.3",
"version": "1.23.0",
"description": "Gradido",
"main": "index.js",
"repository": "git@github.com:gradido/gradido.git",