diff --git a/backend/jest.config.js b/backend/jest.config.js index 81ebbec55..d282f8361 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -7,7 +7,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], coverageThreshold: { global: { - lines: 89, + lines: 90, }, }, setupFiles: ['/test/testSetup.ts'], diff --git a/backend/src/auth/ADMIN_RIGHTS.ts b/backend/src/auth/ADMIN_RIGHTS.ts new file mode 100644 index 000000000..b81ff51d6 --- /dev/null +++ b/backend/src/auth/ADMIN_RIGHTS.ts @@ -0,0 +1,3 @@ +import { RIGHTS } from './RIGHTS' + +export const ADMIN_RIGHTS = [RIGHTS.SET_USER_ROLE, RIGHTS.DELETE_USER, RIGHTS.UNDELETE_USER] diff --git a/backend/src/auth/MODERATOR_RIGHTS.ts b/backend/src/auth/MODERATOR_RIGHTS.ts new file mode 100644 index 000000000..1ff689de6 --- /dev/null +++ b/backend/src/auth/MODERATOR_RIGHTS.ts @@ -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, +] diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index 772c907cb..85ac3e3e7 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -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', } diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts index bc868a199..15ba7b263 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -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] diff --git a/backend/src/auth/USER_RIGHTS.ts b/backend/src/auth/USER_RIGHTS.ts new file mode 100644 index 000000000..9bf9fee93 --- /dev/null +++ b/backend/src/auth/USER_RIGHTS.ts @@ -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, +] diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 98b01cd7a..f4a3795ba 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -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 diff --git a/backend/src/graphql/arg/SetUserRoleArgs.ts b/backend/src/graphql/arg/SetUserRoleArgs.ts new file mode 100644 index 000000000..c076fc8cf --- /dev/null +++ b/backend/src/graphql/arg/SetUserRoleArgs.ts @@ -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 +} diff --git a/backend/src/graphql/directive/isAuthorized.ts b/backend/src/graphql/directive/isAuthorized.ts index e41f41151..59309c91e 100644 --- a/backend/src/graphql/directive/isAuthorized.ts +++ b/backend/src/graphql/directive/isAuthorized.ts @@ -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 = 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') diff --git a/backend/src/graphql/enum/RoleNames.ts b/backend/src/graphql/enum/RoleNames.ts new file mode 100644 index 000000000..c4a9b25cc --- /dev/null +++ b/backend/src/graphql/enum/RoleNames.ts @@ -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 +}) diff --git a/backend/src/graphql/model/AdminUser.ts b/backend/src/graphql/model/AdminUser.ts index 92a22b7f1..d849762bd 100644 --- a/backend/src/graphql/model/AdminUser.ts +++ b/backend/src/graphql/model/AdminUser.ts @@ -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() diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index 5abbdadb7..9e4c0fdf9 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -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[] } diff --git a/backend/src/graphql/model/UserAdmin.ts b/backend/src/graphql/model/UserAdmin.ts index 3e7210874..3063d3763 100644 --- a/backend/src/graphql/model/UserAdmin.ts +++ b/backend/src/graphql/model/UserAdmin.ts @@ -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() diff --git a/backend/src/graphql/model/UserContact.ts b/backend/src/graphql/model/UserContact.ts deleted file mode 100644 index 4a6ed47b6..000000000 --- a/backend/src/graphql/model/UserContact.ts +++ /dev/null @@ -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 -} diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 0bd15d7dc..171af7cf5 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -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')], diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index da58ea096..7b64b548e 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -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 { 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,15 +653,7 @@ export class UserResolver { @Ctx() context: Context, ): Promise { 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 @@ -710,16 +704,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 { - const user = await DbUser.findOne({ where: { id: userId } }) + ): Promise { + 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 +723,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 +826,7 @@ export async function findUserByEmail(email: string): Promise { }) const dbUser = dbUserContact.user dbUser.emailContact = dbUserContact + dbUser.userRoles = await UserRole.find({ where: { userId: dbUser.id } }) return dbUser } @@ -869,3 +854,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 +} diff --git a/backend/src/graphql/resolver/util/modifyUserRole.ts b/backend/src/graphql/resolver/util/modifyUserRole.ts new file mode 100644 index 000000000..f9af28a22 --- /dev/null +++ b/backend/src/graphql/resolver/util/modifyUserRole.ts @@ -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 { + // 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 { + 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') + } +} diff --git a/backend/src/seeds/factory/contributionLink.ts b/backend/src/seeds/factory/contributionLink.ts index 51e970a5c..d03d222c6 100644 --- a/backend/src/seeds/factory/contributionLink.ts +++ b/backend/src/seeds/factory/contributionLink.ts @@ -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, diff --git a/backend/src/seeds/factory/user.ts b/backend/src/seeds/factory/user.ts index d40154e12..3fa5591a2 100644 --- a/backend/src/seeds/factory/user.ts +++ b/backend/src/seeds/factory/user.ts @@ -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 } diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index d604b1f72..87231531f 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -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 } } ` diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 1239e2131..f016102a2 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -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 } } } diff --git a/backend/src/seeds/users/UserInterface.ts b/backend/src/seeds/users/UserInterface.ts index 181eb3f8e..1fb1b128a 100644 --- a/backend/src/seeds/users/UserInterface.ts +++ b/backend/src/seeds/users/UserInterface.ts @@ -9,5 +9,5 @@ export interface UserInterface { language?: string deletedAt?: Date publisherId?: number - isAdmin?: boolean + role?: string } diff --git a/backend/src/seeds/users/peter-lustig.ts b/backend/src/seeds/users/peter-lustig.ts index 0cdc67829..ff1fe065e 100644 --- a/backend/src/seeds/users/peter-lustig.ts +++ b/backend/src/seeds/users/peter-lustig.ts @@ -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, } diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index f96c33470..422c836df 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -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, diff --git a/database/entity/0069-add_user_roles_table/User.ts b/database/entity/0069-add_user_roles_table/User.ts new file mode 100644 index 000000000..a49bb4b87 --- /dev/null +++ b/database/entity/0069-add_user_roles_table/User.ts @@ -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[] +} diff --git a/database/entity/0069-add_user_roles_table/UserRole.ts b/database/entity/0069-add_user_roles_table/UserRole.ts new file mode 100644 index 000000000..118753b20 --- /dev/null +++ b/database/entity/0069-add_user_roles_table/UserRole.ts @@ -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 +} diff --git a/database/entity/User.ts b/database/entity/User.ts index fbb65204c..eb66bdee9 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0059-add_hide_amount_to_users/User' +export { User } from './0069-add_user_roles_table/User' diff --git a/database/entity/UserRole.ts b/database/entity/UserRole.ts new file mode 100644 index 000000000..1ef9a08b2 --- /dev/null +++ b/database/entity/UserRole.ts @@ -0,0 +1 @@ +export { UserRole } from './0069-add_user_roles_table/UserRole' diff --git a/database/entity/index.ts b/database/entity/index.ts index d44029754..b27ac4d61 100644 --- a/database/entity/index.ts +++ b/database/entity/index.ts @@ -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, ] diff --git a/database/migrations/0069-add_user_roles_table.ts b/database/migrations/0069-add_user_roles_table.ts new file mode 100644 index 000000000..8b2970d39 --- /dev/null +++ b/database/migrations/0069-add_user_roles_table.ts @@ -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>) { + 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>) { + // 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;`) +} diff --git a/dht-node/src/config/index.ts b/dht-node/src/config/index.ts index 6f32daaa8..03048b624 100644 --- a/dht-node/src/config/index.ts +++ b/dht-node/src/config/index.ts @@ -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', diff --git a/dht-node/yarn.lock b/dht-node/yarn.lock index f339fd59e..5f42f71a7 100644 --- a/dht-node/yarn.lock +++ b/dht-node/yarn.lock @@ -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== diff --git a/e2e-tests/cypress.config.ts b/e2e-tests/cypress.config.ts index 0e236c478..fd6caa069 100644 --- a/e2e-tests/cypress.config.ts +++ b/e2e-tests/cypress.config.ts @@ -56,7 +56,7 @@ export default defineConfig({ } hasElopage publisherId - isAdmin + roles hideAmountGDD hideAmountGDT __typename diff --git a/federation/src/config/index.ts b/federation/src/config/index.ts index 8b0f52816..72da74aaa 100644 --- a/federation/src/config/index.ts +++ b/federation/src/config/index.ts @@ -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 diff --git a/frontend/src/components/Menu/Sidebar.spec.js b/frontend/src/components/Menu/Sidebar.spec.js index a978be0bb..23c855557 100644 --- a/frontend/src/components/Menu/Sidebar.spec.js +++ b/frontend/src/components/Menu/Sidebar.spec.js @@ -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() }) diff --git a/frontend/src/components/Menu/Sidebar.vue b/frontend/src/components/Menu/Sidebar.vue index b43afd9f9..73eea1015 100644 --- a/frontend/src/components/Menu/Sidebar.vue +++ b/frontend/src/components/Menu/Sidebar.vue @@ -49,12 +49,14 @@ - {{ $t('navigation.admin_area') }} + + {{ $t('navigation.admin_area') }} + { - 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, diff --git a/frontend/src/store/store.test.js b/frontend/src/store/store.test.js index e72b984f7..6a765cc3f 100644 --- a/frontend/src/store/store.test.js +++ b/frontend/src/store/store.test.js @@ -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', () => {