Merge branch 'master' into change-contribution-by-link-text

This commit is contained in:
Hannes Heine 2023-07-25 14:34:29 +02:00 committed by GitHub
commit 9d91786d38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 1318 additions and 413 deletions

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

@ -7,7 +7,7 @@ module.exports = {
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
coverageThreshold: {
global: {
lines: 89,
lines: 90,
},
},
setupFiles: ['<rootDir>/test/testSetup.ts'],

View File

@ -0,0 +1,3 @@
import { RIGHTS } from './RIGHTS'
export const ADMIN_RIGHTS = [RIGHTS.SET_USER_ROLE, RIGHTS.DELETE_USER, RIGHTS.UNDELETE_USER]

View File

@ -0,0 +1,19 @@
import { RIGHTS } from './RIGHTS'
export const MODERATOR_RIGHTS = [
RIGHTS.SEARCH_USERS,
RIGHTS.ADMIN_CREATE_CONTRIBUTION,
RIGHTS.ADMIN_UPDATE_CONTRIBUTION,
RIGHTS.ADMIN_DELETE_CONTRIBUTION,
RIGHTS.ADMIN_LIST_CONTRIBUTIONS,
RIGHTS.CONFIRM_CONTRIBUTION,
RIGHTS.SEND_ACTIVATION_EMAIL,
RIGHTS.LIST_TRANSACTION_LINKS_ADMIN,
RIGHTS.CREATE_CONTRIBUTION_LINK,
RIGHTS.DELETE_CONTRIBUTION_LINK,
RIGHTS.UPDATE_CONTRIBUTION_LINK,
RIGHTS.ADMIN_CREATE_CONTRIBUTION_MESSAGE,
RIGHTS.ADMIN_LIST_ALL_CONTRIBUTION_MESSAGES,
RIGHTS.DENY_CONTRIBUTION,
RIGHTS.ADMIN_OPEN_CREATIONS,
]

View File

@ -1,8 +1,16 @@
export enum RIGHTS {
// Inalienable
LOGIN = 'LOGIN',
COMMUNITIES = 'COMMUNITIES',
CREATE_USER = 'CREATE_USER',
SEND_RESET_PASSWORD_EMAIL = 'SEND_RESET_PASSWORD_EMAIL',
SET_PASSWORD = 'SET_PASSWORD',
QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK',
QUERY_OPT_IN = 'QUERY_OPT_IN',
CHECK_USERNAME = 'CHECK_USERNAME',
// User
VERIFY_LOGIN = 'VERIFY_LOGIN',
BALANCE = 'BALANCE',
COMMUNITIES = 'COMMUNITIES',
LIST_GDT_ENTRIES = 'LIST_GDT_ENTRIES',
EXIST_PID = 'EXIST_PID',
UNSUBSCRIBE_NEWSLETTER = 'UNSUBSCRIBE_NEWSLETTER',
@ -10,15 +18,10 @@ export enum RIGHTS {
TRANSACTION_LIST = 'TRANSACTION_LIST',
SEND_COINS = 'SEND_COINS',
LOGOUT = 'LOGOUT',
CREATE_USER = 'CREATE_USER',
SEND_RESET_PASSWORD_EMAIL = 'SEND_RESET_PASSWORD_EMAIL',
SET_PASSWORD = 'SET_PASSWORD',
QUERY_OPT_IN = 'QUERY_OPT_IN',
UPDATE_USER_INFOS = 'UPDATE_USER_INFOS',
HAS_ELOPAGE = 'HAS_ELOPAGE',
CREATE_TRANSACTION_LINK = 'CREATE_TRANSACTION_LINK',
DELETE_TRANSACTION_LINK = 'DELETE_TRANSACTION_LINK',
QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK',
REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK',
LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS',
GDT_BALANCE = 'GDT_BALANCE',
@ -34,12 +37,8 @@ export enum RIGHTS {
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
OPEN_CREATIONS = 'OPEN_CREATIONS',
USER = 'USER',
CHECK_USERNAME = 'CHECK_USERNAME',
// Admin
// Moderator
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_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION',
@ -54,4 +53,8 @@ export enum RIGHTS {
DENY_CONTRIBUTION = 'DENY_CONTRIBUTION',
ADMIN_OPEN_CREATIONS = 'ADMIN_OPEN_CREATIONS',
ADMIN_LIST_ALL_CONTRIBUTION_MESSAGES = 'ADMIN_LIST_ALL_CONTRIBUTION_MESSAGES',
// Admin
SET_USER_ROLE = 'SET_USER_ROLE',
DELETE_USER = 'DELETE_USER',
UNDELETE_USER = 'UNDELETE_USER',
}

View File

@ -1,40 +1,24 @@
import { INALIENABLE_RIGHTS } from './INALIENABLE_RIGHTS'
import { RIGHTS } from './RIGHTS'
import { Role } from './Role'
import { RoleNames } from '@/graphql/enum/RoleNames'
export const ROLE_UNAUTHORIZED = new Role('unauthorized', INALIENABLE_RIGHTS)
export const ROLE_USER = new Role('user', [
import { ADMIN_RIGHTS } from './ADMIN_RIGHTS'
import { INALIENABLE_RIGHTS } from './INALIENABLE_RIGHTS'
import { MODERATOR_RIGHTS } from './MODERATOR_RIGHTS'
import { Role } from './Role'
import { USER_RIGHTS } from './USER_RIGHTS'
export const ROLE_UNAUTHORIZED = new Role(RoleNames.UNAUTHORIZED, INALIENABLE_RIGHTS)
export const ROLE_USER = new Role(RoleNames.USER, [...INALIENABLE_RIGHTS, ...USER_RIGHTS])
export const ROLE_MODERATOR = new Role(RoleNames.MODERATOR, [
...INALIENABLE_RIGHTS,
RIGHTS.VERIFY_LOGIN,
RIGHTS.BALANCE,
RIGHTS.LIST_GDT_ENTRIES,
RIGHTS.EXIST_PID,
RIGHTS.UNSUBSCRIBE_NEWSLETTER,
RIGHTS.SUBSCRIBE_NEWSLETTER,
RIGHTS.TRANSACTION_LIST,
RIGHTS.SEND_COINS,
RIGHTS.LOGOUT,
RIGHTS.UPDATE_USER_INFOS,
RIGHTS.HAS_ELOPAGE,
RIGHTS.CREATE_TRANSACTION_LINK,
RIGHTS.DELETE_TRANSACTION_LINK,
RIGHTS.REDEEM_TRANSACTION_LINK,
RIGHTS.LIST_TRANSACTION_LINKS,
RIGHTS.GDT_BALANCE,
RIGHTS.CREATE_CONTRIBUTION,
RIGHTS.DELETE_CONTRIBUTION,
RIGHTS.LIST_CONTRIBUTIONS,
RIGHTS.LIST_ALL_CONTRIBUTIONS,
RIGHTS.UPDATE_CONTRIBUTION,
RIGHTS.SEARCH_ADMIN_USERS,
RIGHTS.LIST_CONTRIBUTION_LINKS,
RIGHTS.COMMUNITY_STATISTICS,
RIGHTS.CREATE_CONTRIBUTION_MESSAGE,
RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES,
RIGHTS.OPEN_CREATIONS,
RIGHTS.USER,
...USER_RIGHTS,
...MODERATOR_RIGHTS,
])
export const ROLE_ADMIN = new Role(RoleNames.ADMIN, [
...INALIENABLE_RIGHTS,
...USER_RIGHTS,
...MODERATOR_RIGHTS,
...ADMIN_RIGHTS,
])
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
// TODO from database
export const ROLES = [ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN]
export const ROLES = [ROLE_UNAUTHORIZED, ROLE_USER, ROLE_MODERATOR, ROLE_ADMIN]

View File

@ -0,0 +1,32 @@
import { RIGHTS } from './RIGHTS'
export const USER_RIGHTS = [
RIGHTS.VERIFY_LOGIN,
RIGHTS.BALANCE,
RIGHTS.LIST_GDT_ENTRIES,
RIGHTS.EXIST_PID,
RIGHTS.UNSUBSCRIBE_NEWSLETTER,
RIGHTS.SUBSCRIBE_NEWSLETTER,
RIGHTS.TRANSACTION_LIST,
RIGHTS.SEND_COINS,
RIGHTS.LOGOUT,
RIGHTS.UPDATE_USER_INFOS,
RIGHTS.HAS_ELOPAGE,
RIGHTS.CREATE_TRANSACTION_LINK,
RIGHTS.DELETE_TRANSACTION_LINK,
RIGHTS.REDEEM_TRANSACTION_LINK,
RIGHTS.LIST_TRANSACTION_LINKS,
RIGHTS.GDT_BALANCE,
RIGHTS.CREATE_CONTRIBUTION,
RIGHTS.DELETE_CONTRIBUTION,
RIGHTS.LIST_CONTRIBUTIONS,
RIGHTS.LIST_ALL_CONTRIBUTIONS,
RIGHTS.UPDATE_CONTRIBUTION,
RIGHTS.SEARCH_ADMIN_USERS,
RIGHTS.LIST_CONTRIBUTION_LINKS,
RIGHTS.COMMUNITY_STATISTICS,
RIGHTS.CREATE_CONTRIBUTION_MESSAGE,
RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES,
RIGHTS.OPEN_CREATIONS,
RIGHTS.USER,
]

View File

@ -12,7 +12,7 @@ Decimal.set({
})
const constants = {
DB_VERSION: '0068-community_tables_public_key_length',
DB_VERSION: '0069-add_user_roles_table',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info

View File

@ -0,0 +1,13 @@
import { ArgsType, Field, Int, InputType } from 'type-graphql'
import { RoleNames } from '@enum/RoleNames'
@InputType()
@ArgsType()
export class SetUserRoleArgs {
@Field(() => Int)
userId: number
@Field(() => RoleNames, { nullable: true })
role: RoleNames | null | undefined
}

View File

@ -1,10 +1,12 @@
import { User } from '@entity/User'
import { AuthChecker } from 'type-graphql'
import { RoleNames } from '@enum/RoleNames'
import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS'
import { decode, encode } from '@/auth/JWT'
import { RIGHTS } from '@/auth/RIGHTS'
import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN } from '@/auth/ROLES'
import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN, ROLE_MODERATOR } from '@/auth/ROLES'
import { Context } from '@/server/context'
import { LogError } from '@/server/LogError'
@ -33,10 +35,23 @@ export const isAuthorized: AuthChecker<Context> = async ({ context }, rights) =>
try {
const user = await User.findOneOrFail({
where: { gradidoID: decoded.gradidoID },
relations: ['emailContact'],
withDeleted: true,
relations: ['emailContact', 'userRoles'],
})
context.user = user
context.role = user.isAdmin ? ROLE_ADMIN : ROLE_USER
context.role = ROLE_USER
if (user.userRoles?.length > 0) {
switch (user.userRoles[0].role) {
case RoleNames.ADMIN:
context.role = ROLE_ADMIN
break
case RoleNames.MODERATOR:
context.role = ROLE_MODERATOR
break
default:
context.role = ROLE_USER
}
}
} catch {
// in case the database query fails (user deleted)
throw new LogError('401 Unauthorized')

View File

@ -0,0 +1,13 @@
import { registerEnumType } from 'type-graphql'
export enum RoleNames {
UNAUTHORIZED = 'UNAUTHORIZED',
USER = 'USER',
MODERATOR = 'MODERATOR',
ADMIN = 'ADMIN',
}
registerEnumType(RoleNames, {
name: 'RoleNames', // this one is mandatory
description: 'Possible role names', // this one is optional
})

View File

@ -6,6 +6,7 @@ export class AdminUser {
constructor(user: User) {
this.firstName = user.firstName
this.lastName = user.lastName
this.role = user.userRoles.length > 0 ? user.userRoles[0].role : ''
}
@Field(() => String)
@ -13,6 +14,9 @@ export class AdminUser {
@Field(() => String)
lastName: string
@Field(() => String)
role: string
}
@ObjectType()

View File

@ -18,7 +18,7 @@ export class User {
this.createdAt = user.createdAt
this.language = user.language
this.publisherId = user.publisherId
this.isAdmin = user.isAdmin
this.roles = user.userRoles?.map((userRole) => userRole.role) ?? []
this.klickTipp = null
this.hasElopage = null
this.hideAmountGDD = user.hideAmountGDD
@ -62,12 +62,12 @@ export class User {
@Field(() => Int, { nullable: true })
publisherId: number | null
@Field(() => Date, { nullable: true })
isAdmin: Date | null
@Field(() => KlickTipp, { nullable: true })
klickTipp: KlickTipp | null
@Field(() => Boolean, { nullable: true })
hasElopage: boolean | null
@Field(() => [String])
roles: string[]
}

View File

@ -14,7 +14,7 @@ export class UserAdmin {
this.hasElopage = hasElopage
this.deletedAt = user.deletedAt
this.emailConfirmationSend = emailConfirmationSend
this.isAdmin = user.isAdmin
this.roles = user.userRoles?.map((userRole) => userRole.role) ?? []
}
@Field(() => Int)
@ -44,8 +44,8 @@ export class UserAdmin {
@Field(() => String, { nullable: true })
emailConfirmationSend: string | null
@Field(() => Date, { nullable: true })
isAdmin: Date | null
@Field(() => [String])
roles: string[]
}
@ObjectType()

View File

@ -1,56 +0,0 @@
import { UserContact as dbUserContact } from '@entity/UserContact'
import { ObjectType, Field, Int } from 'type-graphql'
@ObjectType()
export class UserContact {
constructor(userContact: dbUserContact) {
this.id = userContact.id
this.type = userContact.type
this.userId = userContact.userId
this.email = userContact.email
// this.emailVerificationCode = userContact.emailVerificationCode
this.emailOptInTypeId = userContact.emailOptInTypeId
this.emailResendCount = userContact.emailResendCount
this.emailChecked = userContact.emailChecked
this.phone = userContact.phone
this.createdAt = userContact.createdAt
this.updatedAt = userContact.updatedAt
this.deletedAt = userContact.deletedAt
}
@Field(() => Int)
id: number
@Field(() => String)
type: string
@Field(() => Int)
userId: number
@Field(() => String)
email: string
// @Field(() => BigInt, { nullable: true })
// emailVerificationCode: BigInt | null
@Field(() => Int, { nullable: true })
emailOptInTypeId: number | null
@Field(() => Int, { nullable: true })
emailResendCount: number | null
@Field(() => Boolean)
emailChecked: boolean
@Field(() => String, { nullable: true })
phone: string | null
@Field(() => Date)
createdAt: Date
@Field(() => Date, { nullable: true })
updatedAt: Date | null
@Field(() => Date, { nullable: true })
deletedAt: Date | null
}

View File

@ -9,12 +9,15 @@ import { Event as DbEvent } from '@entity/Event'
import { TransactionLink } from '@entity/TransactionLink'
import { User } from '@entity/User'
import { UserContact } from '@entity/UserContact'
import { UserRole } from '@entity/UserRole'
import { UserInputError } from 'apollo-server-express'
import { ApolloServerTestClient } from 'apollo-server-testing'
import { GraphQLError } from 'graphql'
import { v4 as uuidv4, validate as validateUUID, version as versionUUID } from 'uuid'
import { OptInType } from '@enum/OptInType'
import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
import { RoleNames } from '@enum/RoleNames'
import { UserContactType } from '@enum/UserContactType'
import { ContributionLink } from '@model/ContributionLink'
import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers'
@ -140,7 +143,7 @@ describe('UserResolver', () => {
describe('valid input data', () => {
// let loginEmailOptIn: LoginEmailOptIn[]
beforeAll(async () => {
user = await User.find({ relations: ['emailContact'] })
user = await User.find({ relations: ['emailContact', 'userRoles'] })
// loginEmailOptIn = await LoginEmailOptIn.find()
emailVerificationCode = user[0].emailContact.emailVerificationCode.toString()
})
@ -162,7 +165,7 @@ describe('UserResolver', () => {
createdAt: expect.any(Date),
// emailChecked: false,
language: 'de',
isAdmin: null,
userRoles: [],
deletedAt: null,
publisherId: 1234,
referrerId: null,
@ -336,9 +339,16 @@ describe('UserResolver', () => {
})
// make Peter Lustig Admin
const peter = await User.findOneOrFail({ where: { id: user[0].id } })
peter.isAdmin = new Date()
await peter.save()
const peter = await User.findOneOrFail({
where: { id: user[0].id },
relations: ['userRoles'],
})
peter.userRoles = [] as UserRole[]
peter.userRoles[0] = UserRole.create()
peter.userRoles[0].createdAt = new Date()
peter.userRoles[0].role = RoleNames.ADMIN
peter.userRoles[0].userId = peter.id
await peter.userRoles[0].save()
// date statement
const actualDate = new Date()
@ -353,7 +363,6 @@ describe('UserResolver', () => {
validFrom: actualDate,
validTo: futureDate,
})
resetToken()
result = await mutate({
mutation: createUser,
@ -685,13 +694,13 @@ describe('UserResolver', () => {
firstName: 'Bibi',
hasElopage: false,
id: expect.any(Number),
isAdmin: null,
klickTipp: {
newsletterState: false,
},
language: 'de',
lastName: 'Bloxberg',
publisherId: 1234,
roles: [],
},
},
}),
@ -942,7 +951,7 @@ describe('UserResolver', () => {
beforeAll(async () => {
await mutate({ mutation: login, variables })
user = await User.find()
user = await User.find({ relations: ['userRoles'] })
})
afterAll(() => {
@ -962,7 +971,7 @@ describe('UserResolver', () => {
},
hasElopage: false,
publisherId: 1234,
isAdmin: null,
roles: [],
},
},
}),
@ -1403,6 +1412,7 @@ describe('UserResolver', () => {
expect.objectContaining({
firstName: 'Peter',
lastName: 'Lustig',
role: RoleNames.ADMIN,
}),
]),
},
@ -1484,13 +1494,13 @@ describe('UserResolver', () => {
firstName: 'Bibi',
hasElopage: false,
id: expect.any(Number),
isAdmin: null,
klickTipp: {
newsletterState: false,
},
language: 'de',
lastName: 'Bloxberg',
publisherId: 1234,
roles: [],
},
},
}),
@ -1509,7 +1519,10 @@ describe('UserResolver', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
mutate({ mutation: setUserRole, variables: { userId: 1, isAdmin: true } }),
mutate({
mutation: setUserRole,
variables: { userId: 1, role: RoleNames.ADMIN },
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
@ -1519,7 +1532,7 @@ describe('UserResolver', () => {
})
describe('authenticated', () => {
describe('without admin rights', () => {
describe('with user rights', () => {
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
await mutate({
@ -1535,7 +1548,46 @@ describe('UserResolver', () => {
it('returns an error', async () => {
await expect(
mutate({ mutation: setUserRole, variables: { userId: user.id + 1, isAdmin: true } }),
mutate({
mutation: setUserRole,
variables: { userId: user.id + 1, role: RoleNames.ADMIN },
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('with moderator rights', () => {
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
admin = await userFactory(testEnv, peterLustig)
// set Moderator-Role for Peter
const userRole = await UserRole.findOneOrFail({ where: { userId: admin.id } })
userRole.role = RoleNames.MODERATOR
userRole.userId = admin.id
await UserRole.save(userRole)
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
it('returns an error', async () => {
await expect(
mutate({
mutation: setUserRole,
variables: { userId: user.id, role: RoleNames.ADMIN },
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
@ -1546,6 +1598,7 @@ describe('UserResolver', () => {
describe('with admin rights', () => {
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
admin = await userFactory(testEnv, peterLustig)
await mutate({
mutation: login,
@ -1558,11 +1611,33 @@ describe('UserResolver', () => {
resetToken()
})
it('returns user with new moderator-role', async () => {
const result = await mutate({
mutation: setUserRole,
variables: { userId: user.id, role: RoleNames.MODERATOR },
})
expect(result).toEqual(
expect.objectContaining({
data: {
setUserRole: RoleNames.MODERATOR,
},
}),
)
})
describe('user to get a new role does not exist', () => {
afterAll(async () => {
await cleanDB()
resetToken()
})
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: setUserRole, variables: { userId: admin.id + 1, isAdmin: true } }),
mutate({
mutation: setUserRole,
variables: { userId: admin.id + 1, role: RoleNames.ADMIN },
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('Could not find user with given ID')],
@ -1578,19 +1653,55 @@ describe('UserResolver', () => {
describe('change role with success', () => {
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
admin = await userFactory(testEnv, peterLustig)
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('user gets new role', () => {
describe('to admin', () => {
it('returns date string', async () => {
it('returns admin-rolename', async () => {
const result = await mutate({
mutation: setUserRole,
variables: { userId: user.id, isAdmin: true },
variables: { userId: user.id, role: RoleNames.ADMIN },
})
expect(result).toEqual(
expect.objectContaining({
data: {
setUserRole: expect.any(String),
setUserRole: RoleNames.ADMIN,
},
}),
)
})
it('stores the ADMIN_USER_ROLE_SET event in the database', async () => {
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.ADMIN_USER_ROLE_SET,
affectedUserId: user.id,
actingUserId: admin.id,
}),
)
})
})
describe('to moderator', () => {
it('returns date string', async () => {
const result = await mutate({
mutation: setUserRole,
variables: { userId: user.id, role: RoleNames.MODERATOR },
})
expect(result).toEqual(
expect.objectContaining({
data: {
setUserRole: RoleNames.MODERATOR,
},
}),
)
@ -1598,19 +1709,11 @@ describe('UserResolver', () => {
})
it('stores the ADMIN_USER_ROLE_SET event in the database', async () => {
const userConatct = await UserContact.findOneOrFail({
where: { email: 'bibi@bloxberg.de' },
relations: ['user'],
})
const adminConatct = await UserContact.findOneOrFail({
where: { email: 'peter@lustig.de' },
relations: ['user'],
})
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.ADMIN_USER_ROLE_SET,
affectedUserId: userConatct.user.id,
actingUserId: adminConatct.user.id,
affectedUserId: user.id,
actingUserId: admin.id,
}),
)
})
@ -1619,7 +1722,7 @@ describe('UserResolver', () => {
describe('to usual user', () => {
it('returns null', async () => {
await expect(
mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: false } }),
mutate({ mutation: setUserRole, variables: { userId: user.id, role: null } }),
).resolves.toEqual(
expect.objectContaining({
data: {
@ -1633,11 +1736,25 @@ describe('UserResolver', () => {
})
describe('change role with error', () => {
describe('is own role', () => {
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
admin = await userFactory(testEnv, peterLustig)
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('his own role', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: setUserRole, variables: { userId: admin.id, isAdmin: false } }),
mutate({ mutation: setUserRole, variables: { userId: admin.id, role: null } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('Administrator can not change his own role')],
@ -1649,25 +1766,72 @@ describe('UserResolver', () => {
})
})
describe('to not allowed role', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: setUserRole,
variables: { userId: user.id, role: 'unknown rolename' },
}),
).resolves.toEqual(
expect.objectContaining({
errors: [
new UserInputError(
'Variable "$role" got invalid value "unknown rolename"; Value "unknown rolename" does not exist in "RoleNames" enum.',
),
],
}),
)
})
})
describe('user has already role to be set', () => {
describe('to admin', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await mutate({
mutation: setUserRole,
variables: { userId: user.id, isAdmin: true },
variables: { userId: user.id, role: RoleNames.ADMIN },
})
await expect(
mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: true } }),
mutate({
mutation: setUserRole,
variables: { userId: user.id, role: RoleNames.ADMIN },
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('User is already admin')],
errors: [new GraphQLError('User already has role=')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('User is already admin')
expect(logger.error).toBeCalledWith('User already has role=', RoleNames.ADMIN)
})
})
describe('to moderator', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await mutate({
mutation: setUserRole,
variables: { userId: user.id, role: RoleNames.MODERATOR },
})
await expect(
mutate({
mutation: setUserRole,
variables: { userId: user.id, role: RoleNames.MODERATOR },
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('User already has role=')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('User already has role=', RoleNames.MODERATOR)
})
})
@ -1676,10 +1840,10 @@ describe('UserResolver', () => {
jest.clearAllMocks()
await mutate({
mutation: setUserRole,
variables: { userId: user.id, isAdmin: false },
variables: { userId: user.id, role: null },
})
await expect(
mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: false } }),
mutate({ mutation: setUserRole, variables: { userId: user.id, role: null } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('User is already an usual user')],

View File

@ -2,11 +2,12 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { getConnection, IsNull, Not } from '@dbTools/typeorm'
import { getConnection, In } from '@dbTools/typeorm'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
import { TransactionLink as DbTransactionLink } from '@entity/TransactionLink'
import { User as DbUser } from '@entity/User'
import { UserContact as DbUserContact } from '@entity/UserContact'
import { UserRole } from '@entity/UserRole'
import i18n from 'i18n'
import { Resolver, Query, Args, Arg, Authorized, Ctx, Mutation, Int } from 'type-graphql'
import { v4 as uuidv4 } from 'uuid'
@ -14,6 +15,7 @@ import { v4 as uuidv4 } from 'uuid'
import { CreateUserArgs } from '@arg/CreateUserArgs'
import { Paginated } from '@arg/Paginated'
import { SearchUsersFilters } from '@arg/SearchUsersFilters'
import { SetUserRoleArgs } from '@arg/SetUserRoleArgs'
import { UnsecureLoginArgs } from '@arg/UnsecureLoginArgs'
import { UpdateUserInfosArgs } from '@arg/UpdateUserInfosArgs'
import { OptInType } from '@enum/OptInType'
@ -66,6 +68,7 @@ import { getUserCreations } from './util/creations'
import { findUserByIdentifier } from './util/findUserByIdentifier'
import { findUsers } from './util/findUsers'
import { getKlicktippState } from './util/getKlicktippState'
import { setUserRole, deleteUserRole } from './util/modifyUserRole'
import { validateAlias } from './util/validateAlias'
const LANGUAGES = ['de', 'en', 'es', 'fr', 'nl']
@ -159,7 +162,6 @@ export class UserResolver {
const user = new User(dbUser)
logger.debug(`user= ${JSON.stringify(user, null, 2)}`)
i18n.setLocale(user.language)
// Elopage Status & Stored PublisherId
@ -353,7 +355,6 @@ export class UserResolver {
} else {
await EVENT_USER_REGISTER(dbUser)
}
return new User(dbUser)
}
@ -619,8 +620,9 @@ export class UserResolver {
{ currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
): Promise<SearchAdminUsersResult> {
const [users, count] = await DbUser.findAndCount({
relations: ['userRoles'],
where: {
isAdmin: Not(IsNull()),
userRoles: { role: In(['admin', 'moderator']) },
},
order: {
createdAt: order,
@ -628,13 +630,13 @@ export class UserResolver {
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
return {
userCount: count,
userList: users.map((user) => {
return {
firstName: user.firstName,
lastName: user.lastName,
role: user.userRoles ? user.userRoles[0].role : '',
}
}),
}
@ -651,19 +653,9 @@ export class UserResolver {
@Ctx() context: Context,
): Promise<SearchUsersResult> {
const clientTimezoneOffset = getClientTimezoneOffset(context)
const userFields = [
'id',
'firstName',
'lastName',
'emailId',
'emailContact',
'deletedAt',
'isAdmin',
]
const userFields = ['id', 'firstName', 'lastName', 'emailId', 'emailContact', 'deletedAt']
const [users, count] = await findUsers(
userFields.map((fieldName) => {
return 'user.' + fieldName
}),
userFields,
query,
filters ?? null,
currentPage,
@ -710,16 +702,16 @@ export class UserResolver {
}
@Authorized([RIGHTS.SET_USER_ROLE])
@Mutation(() => Date, { nullable: true })
@Mutation(() => String, { nullable: true })
async setUserRole(
@Arg('userId', () => Int)
userId: number,
@Arg('isAdmin', () => Boolean)
isAdmin: boolean,
@Args() { userId, role }: SetUserRoleArgs,
@Ctx()
context: Context,
): Promise<Date | null> {
const user = await DbUser.findOne({ where: { id: userId } })
): Promise<string | null> {
const user = await DbUser.findOne({
where: { id: userId },
relations: ['userRoles'],
})
// user exists ?
if (!user) {
throw new LogError('Could not find user with given ID', userId)
@ -729,27 +721,17 @@ export class UserResolver {
if (moderator.id === userId) {
throw new LogError('Administrator can not change his own role')
}
// change isAdmin
switch (user.isAdmin) {
case null:
if (isAdmin) {
user.isAdmin = new Date()
} else {
throw new LogError('User is already an usual user')
}
break
default:
if (!isAdmin) {
user.isAdmin = null
} else {
throw new LogError('User is already admin')
}
break
// if user role(s) should be deleted by role=null as parameter
if (role === null) {
await deleteUserRole(user)
} else if (isUserInRole(user, role)) {
throw new LogError('User already has role=', role)
} else {
await setUserRole(user, role)
}
await user.save()
await EVENT_ADMIN_USER_ROLE_SET(user, moderator)
const newUser = await DbUser.findOne({ where: { id: userId } })
return newUser ? newUser.isAdmin : null
const newUser = await DbUser.findOne({ where: { id: userId }, relations: ['userRoles'] })
return newUser?.userRoles ? newUser.userRoles[0].role : null
}
@Authorized([RIGHTS.DELETE_USER])
@ -842,6 +824,7 @@ export async function findUserByEmail(email: string): Promise<DbUser> {
})
const dbUser = dbUserContact.user
dbUser.emailContact = dbUserContact
dbUser.userRoles = await UserRole.find({ where: { userId: dbUser.id } })
return dbUser
}
@ -869,3 +852,14 @@ const isEmailVerificationCodeValid = (updatedAt: Date): boolean => {
const canEmailResend = (updatedAt: Date): boolean => {
return !isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_REQUEST_TIME)
}
export function isUserInRole(user: DbUser, role: string | null | undefined): boolean {
if (user && role) {
for (const userRole of user.userRoles) {
if (userRole.role === role) {
return true
}
}
}
return false
}

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

@ -0,0 +1,29 @@
import { User as DbUser } from '@entity/User'
import { UserRole } from '@entity/UserRole'
import { LogError } from '@/server/LogError'
export async function setUserRole(user: DbUser, role: string | null | undefined): Promise<void> {
// if role should be set
if (role) {
// in case user has still no associated userRole
if (user.userRoles.length < 1) {
// instanciate a userRole
user.userRoles.push(UserRole.create())
}
// and initialize the userRole
user.userRoles[0].role = role
user.userRoles[0].userId = user.id
await UserRole.save(user.userRoles[0])
}
}
export async function deleteUserRole(user: DbUser): Promise<void> {
if (user.userRoles.length > 0) {
// remove all roles of the user
await UserRole.delete({ userId: user.id })
user.userRoles.length = 0
} else if (user.userRoles.length === 0) {
throw new LogError('User is already an usual user')
}
}

View File

@ -20,7 +20,6 @@ export const contributionLinkFactory = async (
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
const variables = {
amount: contributionLink.amount,
memo: contributionLink.memo,

View File

@ -3,6 +3,9 @@
import { User } from '@entity/User'
import { ApolloServerTestClient } from 'apollo-server-testing'
import { RoleNames } from '@enum/RoleNames'
import { setUserRole } from '@/graphql/resolver/util/modifyUserRole'
import { createUser, setPassword } from '@/seeds/graphql/mutations'
import { UserInterface } from '@/seeds/users/UserInterface'
@ -17,13 +20,10 @@ export const userFactory = async (
createUser: { id },
},
} = await mutate({ mutation: createUser, variables: user })
// console.log('creatUser:', { id }, { user })
// get user from database
let dbUser = await User.findOneOrFail({ where: { id }, relations: ['emailContact'] })
// console.log('dbUser:', dbUser)
let dbUser = await User.findOneOrFail({ where: { id }, relations: ['emailContact', 'userRoles'] })
const emailContact = dbUser.emailContact
// console.log('emailContact:', emailContact)
if (user.emailChecked) {
await mutate({
@ -33,17 +33,22 @@ export const userFactory = async (
}
// get last changes of user from database
dbUser = await User.findOneOrFail({ where: { id } })
dbUser = await User.findOneOrFail({ where: { id }, relations: ['userRoles'] })
if (user.createdAt || user.deletedAt || user.isAdmin) {
if (user.createdAt || user.deletedAt || user.role) {
if (user.createdAt) dbUser.createdAt = user.createdAt
if (user.deletedAt) dbUser.deletedAt = user.deletedAt
if (user.isAdmin) dbUser.isAdmin = new Date()
if (user.role && (user.role === RoleNames.ADMIN || user.role === RoleNames.MODERATOR)) {
await setUserRole(dbUser, user.role)
}
await dbUser.save()
}
// get last changes of user from database
// dbUser = await User.findOneOrFail({ id }, { withDeleted: true })
dbUser = await User.findOneOrFail({
where: { id },
withDeleted: true,
relations: ['emailContact', 'userRoles'],
})
return dbUser
}

View File

@ -119,8 +119,8 @@ export const confirmContribution = gql`
`
export const setUserRole = gql`
mutation ($userId: Int!, $isAdmin: Boolean!) {
setUserRole(userId: $userId, isAdmin: $isAdmin)
mutation ($userId: Int!, $role: RoleNames) {
setUserRole(userId: $userId, role: $role)
}
`
@ -321,7 +321,7 @@ export const login = gql`
}
hasElopage
publisherId
isAdmin
roles
}
}
`

View File

@ -11,7 +11,7 @@ export const verifyLogin = gql`
}
hasElopage
publisherId
isAdmin
roles
}
}
`
@ -94,7 +94,7 @@ export const searchUsers = gql`
hasElopage
emailConfirmationSend
deletedAt
isAdmin
roles
}
}
}
@ -323,6 +323,7 @@ export const searchAdminUsers = gql`
userList {
firstName
lastName
role
}
}
}

View File

@ -9,5 +9,5 @@ export interface UserInterface {
language?: string
deletedAt?: Date
publisherId?: number
isAdmin?: boolean
role?: string
}

View File

@ -1,3 +1,5 @@
import { RoleNames } from '@enum/RoleNames'
import { UserInterface } from './UserInterface'
export const peterLustig: UserInterface = {
@ -8,5 +10,5 @@ export const peterLustig: UserInterface = {
createdAt: new Date('2020-11-25T10:48:43'),
emailChecked: true,
language: 'de',
isAdmin: true,
role: RoleNames.ADMIN,
}

View File

@ -26,7 +26,7 @@ const communityDbUser: dbUser = {
createdAt: new Date(),
// emailChecked: false,
language: '',
isAdmin: null,
userRoles: [],
publisherId: 0,
// default password encryption type
passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD,

View File

@ -0,0 +1,120 @@
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
DeleteDateColumn,
OneToMany,
JoinColumn,
OneToOne,
} from 'typeorm'
import { Contribution } from '../Contribution'
import { ContributionMessage } from '../ContributionMessage'
import { UserContact } from '../UserContact'
import { UserRole } from './UserRole'
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({
name: 'gradido_id',
length: 36,
nullable: false,
collation: 'utf8mb4_unicode_ci',
})
gradidoID: string
@Column({
name: 'alias',
length: 20,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
alias: string
@OneToOne(() => UserContact, (emailContact: UserContact) => emailContact.user)
@JoinColumn({ name: 'email_id' })
emailContact: UserContact
@Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null })
emailId: number | null
@Column({
name: 'first_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
firstName: string
@Column({
name: 'last_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
lastName: string
@Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP(3)', nullable: false })
createdAt: Date
@DeleteDateColumn({ name: 'deleted_at', nullable: true })
deletedAt: Date | null
@Column({ type: 'bigint', default: 0, unsigned: true })
password: BigInt
@Column({
name: 'password_encryption_type',
type: 'int',
unsigned: true,
nullable: false,
default: 0,
})
passwordEncryptionType: number
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
language: string
@Column({ type: 'bool', default: false })
hideAmountGDD: boolean
@Column({ type: 'bool', default: false })
hideAmountGDT: boolean
@OneToMany(() => UserRole, (userRole) => userRole.user)
@JoinColumn({ name: 'user_id' })
userRoles: UserRole[]
@Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null })
referrerId?: number | null
@Column({
name: 'contribution_link_id',
type: 'int',
unsigned: true,
nullable: true,
default: null,
})
contributionLinkId?: number | null
@Column({ name: 'publisher_id', default: 0 })
publisherId: number
@OneToMany(() => Contribution, (contribution) => contribution.user)
@JoinColumn({ name: 'user_id' })
contributions?: Contribution[]
@OneToMany(() => ContributionMessage, (message) => message.user)
@JoinColumn({ name: 'user_id' })
messages?: ContributionMessage[]
@OneToMany(() => UserContact, (userContact: UserContact) => userContact.user)
@JoinColumn({ name: 'user_id' })
userContacts?: UserContact[]
}

View File

@ -0,0 +1,24 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'
import { User } from '../User'
@Entity('user_roles', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class UserRole extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'user_id', type: 'int', unsigned: true, nullable: false })
userId: number
@Column({ length: 40, nullable: false, collation: 'utf8mb4_unicode_ci' })
role: string
@Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP(3)', nullable: false })
createdAt: Date
@Column({ name: 'updated_at', nullable: true, default: null, type: 'datetime' })
updatedAt: Date | null
@ManyToOne(() => User, (user) => user.userRoles)
@JoinColumn({ name: 'user_id' })
user: User
}

View File

@ -1 +1 @@
export { User } from './0059-add_hide_amount_to_users/User'
export { User } from './0069-add_user_roles_table/User'

View File

@ -0,0 +1 @@
export { UserRole } from './0069-add_user_roles_table/UserRole'

View File

@ -11,6 +11,7 @@ import { Event } from './Event'
import { ContributionMessage } from './ContributionMessage'
import { Community } from './Community'
import { FederatedCommunity } from './FederatedCommunity'
import { UserRole } from './UserRole'
export const entities = [
Community,
@ -26,4 +27,5 @@ export const entities = [
TransactionLink,
User,
UserContact,
UserRole,
]

View File

@ -0,0 +1,43 @@
/* 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(`
CREATE TABLE user_roles (
id int unsigned NOT NULL AUTO_INCREMENT,
user_id int(10) unsigned NOT NULL,
role varchar(40) NOT NULL,
created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at datetime(3),
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`)
// insert values from users table with users.is_admin in new user_roles table
await queryFn(`
INSERT INTO user_roles
(user_id, role, created_at, updated_at)
SELECT u.id, 'ADMIN', u.is_admin, null
FROM users u
WHERE u.is_admin IS NOT NULL;`)
// remove column is_admin from users table
await queryFn('ALTER TABLE users DROP COLUMN is_admin;')
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// first add column is_admin in users table
await queryFn(
'ALTER TABLE users ADD COLUMN is_admin datetime(3) NULL DEFAULT NULL AFTER language;',
)
// 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"`,
)
for (const id in roles) {
const role = roles[id]
const isAdminDate = new Date(role.created_at).toISOString().slice(0, 19).replace('T', ' ')
await queryFn(`UPDATE users SET is_admin = "${isAdminDate}" WHERE id = "${role.user_id}"`)
}
await queryFn(`DROP TABLE user_roles;`)
}

View File

@ -4,7 +4,7 @@ import dotenv from 'dotenv'
dotenv.config()
const constants = {
DB_VERSION: '0068-community_tables_public_key_length',
DB_VERSION: '0069-add_user_roles_table',
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info',

View File

@ -252,6 +252,13 @@
dependencies:
"@babel/helper-plugin-utils" "^7.19.0"
"@babel/runtime@^7.21.0":
version "7.22.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.6.tgz#57d64b9ae3cff1d67eb067ae117dac087f5bd438"
integrity sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==
dependencies:
regenerator-runtime "^0.13.11"
"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.3.3":
version "7.20.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8"
@ -672,7 +679,7 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
"@sqltools/formatter@^1.2.2":
"@sqltools/formatter@^1.2.5":
version "1.2.5"
resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.5.tgz#3abc203c79b8c3e90fd6c156a0c62d5403520e12"
integrity sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==
@ -835,11 +842,6 @@
dependencies:
"@types/yargs-parser" "*"
"@types/zen-observable@0.8.3":
version "0.8.3"
resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.3.tgz#781d360c282436494b32fe7d9f7f8e64b3118aa3"
integrity sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw==
"@typescript-eslint/eslint-plugin@^5.57.1":
version "5.59.9"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.9.tgz#2604cfaf2b306e120044f901e20c8ed926debf15"
@ -1028,7 +1030,7 @@ anymatch@^3.0.3, anymatch@~3.1.2:
normalize-path "^3.0.0"
picomatch "^2.0.4"
app-root-path@^3.0.0:
app-root-path@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.1.0.tgz#5971a2fc12ba170369a7a1ef018c71e6e47c2e86"
integrity sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==
@ -1221,6 +1223,13 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
brace-expansion@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
dependencies:
balanced-match "^1.0.0"
braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
@ -1328,7 +1337,7 @@ chalk@^2.0.0:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^4.0.0, chalk@^4.1.0:
chalk@^4.0.0, chalk@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
@ -1518,12 +1527,19 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
date-fns@^2.29.3:
version "2.30.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
dependencies:
"@babel/runtime" "^7.21.0"
date-format@^4.0.14:
version "4.0.14"
resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400"
integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@ -1680,10 +1696,10 @@ dotenv@10.0.0, dotenv@^10.0.0:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==
dotenv@^8.2.0:
version "8.6.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"
integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==
dotenv@^16.0.3:
version "16.3.1"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
electron-to-chromium@^1.4.251:
version "1.4.284"
@ -2297,7 +2313,7 @@ glob-parent@^6.0.2:
dependencies:
is-glob "^4.0.3"
glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
@ -2309,6 +2325,17 @@ glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e"
integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^5.0.1"
once "^1.3.0"
globals@^11.1.0:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
@ -2362,7 +2389,7 @@ graceful-fs@^4.2.4:
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
"gradido-database@file:../database":
version "1.21.0"
version "1.22.3"
dependencies:
"@types/uuid" "^8.3.4"
cross-env "^7.0.3"
@ -2372,7 +2399,7 @@ graceful-fs@^4.2.4:
mysql2 "^2.3.0"
reflect-metadata "^0.1.13"
ts-mysql-migrate "^1.0.2"
typeorm "^0.2.38"
typeorm "^0.3.16"
uuid "^8.3.2"
grapheme-splitter@^1.0.4:
@ -3205,7 +3232,7 @@ js-yaml@^3.13.1:
argparse "^1.0.7"
esprima "^4.0.0"
js-yaml@^4.0.0, js-yaml@^4.1.0:
js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
@ -3450,15 +3477,22 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
dependencies:
brace-expansion "^1.1.7"
minimatch@^5.0.1:
version "5.1.6"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
dependencies:
brace-expansion "^2.0.1"
minimist@^1.2.0, minimist@^1.2.6:
version "1.2.7"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
mkdirp@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
mkdirp@^2.1.3:
version "2.1.6"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-2.1.6.tgz#964fbcb12b2d8c5d6fbc62a963ac95a273e2cc19"
integrity sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==
ms@2.1.2:
version "2.1.2"
@ -3940,6 +3974,11 @@ reflect-metadata@^0.1.13:
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==
regenerator-runtime@^0.13.11:
version "0.13.11"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
regexp-tree@~0.1.1:
version "0.1.27"
resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd"
@ -4072,11 +4111,6 @@ safety-catch@^1.0.1:
resolved "https://registry.yarnpkg.com/safety-catch/-/safety-catch-1.0.2.tgz#d64cbd57fd601da91c356b6ab8902f3e449a7a4b"
integrity sha512-C1UYVZ4dtbBxEtvOcpjBaaD27nP8MlvyAQEp2fOTOEe6pfUpk1cDUxij6BR1jZup6rSyUTaBBplK7LanskrULA==
sax@>=0.6.0:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
saxes@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d"
@ -4617,7 +4651,7 @@ tslib@^1.8.1:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.1.0, tslib@^2.5.0:
tslib@^2.5.0:
version "2.5.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913"
integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==
@ -4665,28 +4699,26 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"
typeorm@^0.2.38:
version "0.2.45"
resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.2.45.tgz#e5bbb3af822dc4646bad96cfa48cd22fa4687cea"
integrity sha512-c0rCO8VMJ3ER7JQ73xfk0zDnVv0WDjpsP6Q1m6CVKul7DB9iVdWLRjPzc8v2eaeBuomsbZ2+gTaYr8k1gm3bYA==
typeorm@^0.3.16:
version "0.3.17"
resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.17.tgz#a73c121a52e4fbe419b596b244777be4e4b57949"
integrity sha512-UDjUEwIQalO9tWw9O2A4GU+sT3oyoUXheHJy4ft+RFdnRdQctdQ34L9SqE2p7LdwzafHx1maxT+bqXON+Qnmig==
dependencies:
"@sqltools/formatter" "^1.2.2"
app-root-path "^3.0.0"
"@sqltools/formatter" "^1.2.5"
app-root-path "^3.1.0"
buffer "^6.0.3"
chalk "^4.1.0"
chalk "^4.1.2"
cli-highlight "^2.1.11"
debug "^4.3.1"
dotenv "^8.2.0"
glob "^7.1.6"
js-yaml "^4.0.0"
mkdirp "^1.0.4"
date-fns "^2.29.3"
debug "^4.3.4"
dotenv "^16.0.3"
glob "^8.1.0"
mkdirp "^2.1.3"
reflect-metadata "^0.1.13"
sha.js "^2.4.11"
tslib "^2.1.0"
uuid "^8.3.2"
xml2js "^0.4.23"
yargs "^17.0.1"
zen-observable-ts "^1.0.0"
tslib "^2.5.0"
uuid "^9.0.0"
yargs "^17.6.2"
typescript@^4.9.4:
version "4.9.4"
@ -4767,6 +4799,11 @@ uuid@^8.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
v8-compile-cache-lib@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
@ -4895,19 +4932,6 @@ xml-name-validator@^3.0.0:
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
xml2js@^0.4.23:
version "0.4.23"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==
dependencies:
sax ">=0.6.0"
xmlbuilder "~11.0.0"
xmlbuilder@~11.0.0:
version "11.0.1"
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==
xmlchars@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
@ -4956,7 +4980,7 @@ yargs@^16.0.0, yargs@^16.2.0:
y18n "^5.0.5"
yargs-parser "^20.2.2"
yargs@^17.0.1:
yargs@^17.6.2:
version "17.7.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
@ -4978,16 +5002,3 @@ yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zen-observable-ts@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.1.0.tgz#2d1aa9d79b87058e9b75698b92791c1838551f83"
integrity sha512-1h4zlLSqI2cRLPJUHJFL8bCWHhkpuXkF+dbGkRaWjgDIG26DmzyshUMrdV/rL3UnR+mhaX4fRq8LPouq0MYYIA==
dependencies:
"@types/zen-observable" "0.8.3"
zen-observable "0.8.15"
zen-observable@0.8.15:
version "0.8.15"
resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15"
integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==

View File

@ -56,7 +56,7 @@ export default defineConfig({
}
hasElopage
publisherId
isAdmin
roles
hideAmountGDD
hideAmountGDT
__typename

View File

@ -11,7 +11,7 @@ Decimal.set({
*/
const constants = {
DB_VERSION: '0068-community_tables_public_key_length',
DB_VERSION: '0069-add_user_roles_table',
// DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info

View File

@ -14,7 +14,7 @@ describe('Sidebar', () => {
$store: {
state: {
hasElopage: true,
isAdmin: false,
roles: [],
},
},
}
@ -83,7 +83,7 @@ describe('Sidebar', () => {
describe('for admin users', () => {
beforeAll(() => {
mocks.$store.state.isAdmin = true
mocks.$store.state.roles = ['admin']
wrapper = Wrapper()
})

View File

@ -49,12 +49,14 @@
</b-nav-item>
<b-nav-item
class="mb-3 text-light"
v-if="$store.state.isAdmin"
v-if="$store.state.roles && $store.state.roles.length > 0"
@click="$emit('admin')"
active-class="activeRoute"
>
<b-icon icon="shield-check" aria-hidden="true"></b-icon>
<span class="ml-2">{{ $t('navigation.admin_area') }}</span>
<span class="ml-2">
{{ $t('navigation.admin_area') }}
</span>
</b-nav-item>
<b-nav-item
class="font-weight-bold"

View File

@ -156,7 +156,7 @@ export const login = gql`
}
hasElopage
publisherId
isAdmin
roles
hideAmountGDD
hideAmountGDT
}

View File

@ -13,7 +13,7 @@ export const verifyLogin = gql`
}
hasElopage
publisherId
isAdmin
roles
hideAmountGDD
hideAmountGDT
}

View File

@ -47,6 +47,7 @@ const mocks = {
firstName: 'User',
lastName: 'Example',
token: 'valid-token',
roles: [],
},
},
$i18n: {

View File

@ -40,8 +40,8 @@ export const mutations = {
if (isNaN(pubId)) pubId = null
state.publisherId = pubId
},
isAdmin: (state, isAdmin) => {
state.isAdmin = !!isAdmin
roles(state, roles) {
state.roles = roles
},
hasElopage: (state, hasElopage) => {
state.hasElopage = hasElopage
@ -70,7 +70,7 @@ export const actions = {
commit('newsletterState', data.klickTipp.newsletterState)
commit('hasElopage', data.hasElopage)
commit('publisherId', data.publisherId)
commit('isAdmin', data.isAdmin)
commit('roles', data.roles)
commit('hideAmountGDD', data.hideAmountGDD)
commit('hideAmountGDT', data.hideAmountGDT)
commit('setDarkMode', data.darkMode)
@ -84,7 +84,7 @@ export const actions = {
commit('newsletterState', null)
commit('hasElopage', false)
commit('publisherId', null)
commit('isAdmin', false)
commit('roles', null)
commit('hideAmountGDD', false)
commit('hideAmountGDT', true)
commit('email', '')
@ -111,7 +111,7 @@ try {
// username: '',
token: null,
tokenTime: null,
isAdmin: false,
roles: [],
newsletterState: null,
hasElopage: false,
publisherId: null,

View File

@ -29,7 +29,7 @@ const {
username,
newsletterState,
publisherId,
isAdmin,
roles,
hasElopage,
hideAmountGDD,
hideAmountGDT,
@ -136,11 +136,11 @@ describe('Vuex store', () => {
})
})
describe('isAdmin', () => {
it('sets the state of isAdmin', () => {
const state = { isAdmin: null }
isAdmin(state, true)
expect(state.isAdmin).toEqual(true)
describe('roles', () => {
it('sets the state of roles', () => {
const state = { roles: [] }
roles(state, ['admin'])
expect(state.roles).toEqual(['admin'])
})
})
@ -192,7 +192,7 @@ describe('Vuex store', () => {
},
hasElopage: false,
publisherId: 1234,
isAdmin: true,
roles: ['admin'],
hideAmountGDD: false,
hideAmountGDT: true,
}
@ -242,9 +242,9 @@ describe('Vuex store', () => {
expect(commit).toHaveBeenNthCalledWith(8, 'publisherId', 1234)
})
it('commits isAdmin', () => {
it('commits roles', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(9, 'isAdmin', true)
expect(commit).toHaveBeenNthCalledWith(9, 'roles', ['admin'])
})
it('commits hideAmountGDD', () => {
@ -307,9 +307,9 @@ describe('Vuex store', () => {
expect(commit).toHaveBeenNthCalledWith(8, 'publisherId', null)
})
it('commits isAdmin', () => {
it('commits roles', () => {
logout({ commit, state })
expect(commit).toHaveBeenNthCalledWith(9, 'isAdmin', false)
expect(commit).toHaveBeenNthCalledWith(9, 'roles', null)
})
it('commits hideAmountGDD', () => {