diff --git a/backend/src/graphql/directive/isAuthorized.ts b/backend/src/graphql/directive/isAuthorized.ts index 94aa60ffc..f1e26df4a 100644 --- a/backend/src/graphql/directive/isAuthorized.ts +++ b/backend/src/graphql/directive/isAuthorized.ts @@ -33,11 +33,12 @@ export const isAuthorized: AuthChecker = async ({ context }, rights) => try { const user = await User.findOneOrFail({ where: { gradidoID: decoded.gradidoID }, - relations: ['emailContact', 'userRole'], + relations: ['emailContact', 'userRoles'], }) + console.log('isAuthorized user=', user) context.user = user - context.role = user.userRole - ? user.userRole.role === ROLE_NAMES.ROLE_NAME_ADMIN + context.role = user.userRoles + ? user.userRoles[0].role === ROLE_NAMES.ROLE_NAME_ADMIN ? ROLE_ADMIN : ROLE_MODERATOR : ROLE_USER diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index 3558a0840..0a4993d28 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -1,7 +1,8 @@ -import { ROLE_NAMES } from '@/auth/ROLES' import { User as dbUser } from '@entity/User' import { ObjectType, Field, Int } from 'type-graphql' +import { ROLE_NAMES } from '@/auth/ROLES' + import { KlickTipp } from './KlickTipp' @ObjectType() @@ -19,18 +20,11 @@ export class User { this.createdAt = user.createdAt this.language = user.language this.publisherId = user.publisherId - if (user.userRole) { - switch (user.userRole.role) { - case ROLE_NAMES.ROLE_NAME_ADMIN: - this.isAdmin = user.userRole.createdAt - break - case ROLE_NAMES.ROLE_NAME_MODERATOR: - this.isModerator = user.userRole.createdAt - break - default: - this.isAdmin = null - this.isModerator = null - } + if (user.userRoles) { + this.roles = [] as string[] + user.userRoles.forEach((userRole) => { + this.roles?.push(userRole.role) + }) } this.klickTipp = null this.hasElopage = null @@ -75,15 +69,34 @@ export class User { @Field(() => Int, { nullable: true }) publisherId: number | null - @Field(() => Date, { nullable: true }) - isAdmin: Date | null - - @Field(() => Date, { nullable: true }) - isModerator: Date | null - @Field(() => KlickTipp, { nullable: true }) klickTipp: KlickTipp | null @Field(() => Boolean, { nullable: true }) hasElopage: boolean | null + + @Field(() => [String], { nullable: true }) + roles: string[] | null +} + +export function isAdmin(user: User): boolean { + if (user.roles) { + for (const role of user.roles) { + if (role === ROLE_NAMES.ROLE_NAME_ADMIN) { + return true + } + } + } + return false +} + +export function isModerator(user: User): boolean { + if (user.roles) { + for (const role of user.roles) { + if (role === ROLE_NAMES.ROLE_NAME_MODERATOR) { + return true + } + } + } + return false } diff --git a/backend/src/graphql/model/UserAdmin.ts b/backend/src/graphql/model/UserAdmin.ts index ff1011468..d12b629fa 100644 --- a/backend/src/graphql/model/UserAdmin.ts +++ b/backend/src/graphql/model/UserAdmin.ts @@ -2,6 +2,8 @@ import { User } from '@entity/User' import { Decimal } from 'decimal.js-light' import { ObjectType, Field, Int } from 'type-graphql' +import { ROLE_NAMES } from '@/auth/ROLES' + @ObjectType() export class UserAdmin { constructor(user: User, creation: Decimal[], hasElopage: boolean, emailConfirmationSend: string) { @@ -14,8 +16,18 @@ export class UserAdmin { this.hasElopage = hasElopage this.deletedAt = user.deletedAt this.emailConfirmationSend = emailConfirmationSend - if (user.userRole) { - this.isAdmin = user.userRole?.createdAt + if (user.userRoles) { + switch (user.userRoles[0].role) { + case ROLE_NAMES.ROLE_NAME_ADMIN: + this.isAdmin = user.userRoles[0].createdAt + break + case ROLE_NAMES.ROLE_NAME_MODERATOR: + this.isModerator = user.userRoles[0].createdAt + break + default: + this.isAdmin = null + this.isModerator = null + } } } @@ -48,6 +60,9 @@ export class UserAdmin { @Field(() => Date, { nullable: true }) isAdmin: Date | null + + @Field(() => Date, { nullable: true }) + isModerator: Date | null } @ObjectType() diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 9927af91e..c37e0f48e 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -9,6 +9,7 @@ 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 { ApolloServerTestClient } from 'apollo-server-testing' import { GraphQLError } from 'graphql' import { v4 as uuidv4, validate as validateUUID, version as versionUUID } from 'uuid' @@ -21,6 +22,7 @@ import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/help import { logger, i18n as localization } from '@test/testSetup' import { subscribe } from '@/apis/KlicktippController' +import { ROLE_NAMES } from '@/auth/ROLES' import { CONFIG } from '@/config' import { sendAccountActivationEmail, @@ -62,8 +64,6 @@ import { peterLustig } from '@/seeds/users/peter-lustig' import { stephenHawking } from '@/seeds/users/stephen-hawking' import { printTimeDuration } from '@/util/time' import { objectValuesToArray } from '@/util/utilities' -import { UserRole } from '@entity/UserRole' -import { ROLE_NAMES } from '@/auth/ROLES' jest.mock('@/emails/sendEmailVariants', () => { const originalModule = jest.requireActual('@/emails/sendEmailVariants') @@ -142,7 +142,7 @@ describe('UserResolver', () => { describe('valid input data', () => { // let loginEmailOptIn: LoginEmailOptIn[] beforeAll(async () => { - user = await User.find({ relations: ['emailContact', 'userRole'] }) + user = await User.find({ relations: ['emailContact', 'userRoles'] }) // loginEmailOptIn = await LoginEmailOptIn.find() emailVerificationCode = user[0].emailContact.emailVerificationCode.toString() }) @@ -164,7 +164,7 @@ describe('UserResolver', () => { createdAt: expect.any(Date), // emailChecked: false, language: 'de', - userRole: null, + userRoles: null, deletedAt: null, publisherId: 1234, referrerId: null, @@ -338,16 +338,16 @@ describe('UserResolver', () => { // make Peter Lustig Admin let peter = await User.findOneOrFail({ where: { id: user[0].id }, - relations: ['userRole'], + relations: ['userRoles'], }) console.log('vorher peter=', peter) await mutate({ mutation: setUserRole, - variables: { userId: user[0].id, isAdmin: true }, + variables: { userId: user[0].id, role: ROLE_NAMES.ROLE_NAME_ADMIN }, }) peter = await User.findOneOrFail({ where: { id: user[0].id }, - relations: ['userRole'], + relations: ['userRoles'], }) console.log('nachher peter=', peter) @@ -704,7 +704,7 @@ describe('UserResolver', () => { firstName: 'Bibi', hasElopage: false, id: expect.any(Number), - userRole: null, + userRoles: null, klickTipp: { newsletterState: false, }, @@ -961,7 +961,7 @@ describe('UserResolver', () => { beforeAll(async () => { await mutate({ mutation: login, variables }) - user = await User.find({ relations: ['userRole'] }) + user = await User.find({ relations: ['userRoles'] }) }) afterAll(() => { @@ -981,7 +981,7 @@ describe('UserResolver', () => { }, hasElopage: false, publisherId: 1234, - userRole: null, + userRoles: null, }, }, }), @@ -1501,7 +1501,7 @@ describe('UserResolver', () => { firstName: 'Bibi', hasElopage: false, id: expect.any(Number), - userRole: null, + userRoles: null, klickTipp: { newsletterState: false, }, @@ -1526,7 +1526,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: ROLE_NAMES.ROLE_NAME_ADMIN }, + }), ).resolves.toEqual( expect.objectContaining({ errors: [new GraphQLError('401 Unauthorized')], @@ -1552,7 +1555,10 @@ 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: ROLE_NAMES.ROLE_NAME_ADMIN }, + }), ).resolves.toEqual( expect.objectContaining({ errors: [new GraphQLError('401 Unauthorized')], @@ -1579,7 +1585,10 @@ describe('UserResolver', () => { 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: ROLE_NAMES.ROLE_NAME_ADMIN }, + }), ).resolves.toEqual( expect.objectContaining({ errors: [new GraphQLError('Could not find user with given ID')], @@ -1602,7 +1611,7 @@ describe('UserResolver', () => { it('returns date string', async () => { const result = await mutate({ mutation: setUserRole, - variables: { userId: user.id, isAdmin: true }, + variables: { userId: user.id, role: ROLE_NAMES.ROLE_NAME_ADMIN }, }) expect(result).toEqual( expect.objectContaining({ @@ -1636,7 +1645,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: { @@ -1654,7 +1663,7 @@ describe('UserResolver', () => { 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')], @@ -1672,10 +1681,13 @@ describe('UserResolver', () => { jest.clearAllMocks() await mutate({ mutation: setUserRole, - variables: { userId: user.id, isAdmin: true }, + variables: { userId: user.id, role: ROLE_NAMES.ROLE_NAME_ADMIN }, }) await expect( - mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: true } }), + mutate({ + mutation: setUserRole, + variables: { userId: user.id, role: ROLE_NAMES.ROLE_NAME_ADMIN }, + }), ).resolves.toEqual( expect.objectContaining({ errors: [new GraphQLError('User is already admin')], @@ -1693,10 +1705,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 6124f1a0d..274b3fa1d 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -706,18 +706,27 @@ 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, + @Arg('role', () => String) + role: string, @Ctx() context: Context, - ): Promise { + ): Promise { + switch (role) { + case null: + case ROLE_NAMES.ROLE_NAME_ADMIN: + case ROLE_NAMES.ROLE_NAME_MODERATOR: + logger.debug('setUserRole=', role) + break + default: + throw new LogError('Not allowed to set user role=', role) + } const user = await DbUser.findOne({ where: { id: userId }, - relations: ['userRole'], + relations: ['userRoles'], }) // user exists ? if (!user) { @@ -728,37 +737,32 @@ export class UserResolver { if (moderator.id === userId) { throw new LogError('Administrator can not change his own role') } - // change userRole - switch (user.userRole) { - case null: - if (isAdmin) { - user.userRole = UserRole.create() - user.userRole.createdAt = new Date() - user.userRole.role = ROLE_NAMES.ROLE_NAME_ADMIN - user.userRole.userId = user.id - } else { - throw new LogError('User is already an usual user') + if (isUserInRole(user, role)) { + throw new LogError('User already has role=', role) + } + // if user role should be deleted by role=null as parameter + if (role === null && user.userRoles) { + for (const usrRole of user.userRoles) { + await UserRole.delete(usrRole) + } + user.userRoles = undefined + } else { + if (!isUserInRole(user, role)) { + if (user.userRoles === undefined) { + user.userRoles = [] as UserRole[] + user.userRoles[0] = UserRole.create() } - break - default: - if (!isAdmin) { - if (user.userRole) { - await UserRole.delete(user.userRole) - } - user.userRole = undefined - } else { - throw new LogError('User is already admin') - } - break + user.userRoles[0].createdAt = new Date() + user.userRoles[0].role = role + user.userRoles[0].userId = user.id + } else { + throw new LogError('User already is in role=', role) + } } await user.save() await EVENT_ADMIN_USER_ROLE_SET(user, moderator) - const newUser = await DbUser.findOne({ id: userId }) - return newUser - ? newUser.userRole && newUser.userRole.role === ROLE_NAMES.ROLE_NAME_ADMIN - ? newUser.userRole.createdAt - : null - : null + const newUser = await DbUser.findOne({ id: userId }, { relations: ['userRoles'] }) + return newUser?.userRoles ? newUser.userRoles[0].role : null } @Authorized([RIGHTS.DELETE_USER]) @@ -850,7 +854,7 @@ export async function findUserByEmail(email: string): Promise { }) const dbUser = dbUserContact.user dbUser.emailContact = dbUserContact - dbUser.userRole = await UserRole.findOne({ userId: dbUser.id }) + dbUser.userRoles = await UserRole.find({ userId: dbUser.id }) return dbUser } @@ -875,3 +879,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): boolean { + if (user?.userRoles) { + for (const usrRole of user.userRoles) { + if (usrRole.role === role) { + return true + } + } + } + return false +} diff --git a/backend/src/seeds/factory/contributionLink.ts b/backend/src/seeds/factory/contributionLink.ts index 51e970a5c..cbbe02ec8 100644 --- a/backend/src/seeds/factory/contributionLink.ts +++ b/backend/src/seeds/factory/contributionLink.ts @@ -20,7 +20,7 @@ export const contributionLinkFactory = async ( mutation: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, }) - + console.log('contributionlinkfactory user=', user) 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 74c6aea26..fb84787df 100644 --- a/backend/src/seeds/factory/user.ts +++ b/backend/src/seeds/factory/user.ts @@ -37,15 +37,20 @@ export const userFactory = async ( // get last changes of user from database dbUser = await User.findOneOrFail({ id }, { relations: ['emailContact', 'userRole'] }) - 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.userRole = UserRole.create() - dbUser.userRole.createdAt = new Date() - dbUser.userRole.role = ROLE_NAMES.ROLE_NAME_ADMIN - dbUser.userRole.userId = dbUser.id - await dbUser.userRole.save() + if (user.role) { + dbUser.userRoles = [] as UserRole[] + dbUser.userRoles[0] = UserRole.create() + dbUser.userRoles[0].createdAt = new Date() + if (user.role === ROLE_NAMES.ROLE_NAME_ADMIN) { + dbUser.userRoles[0].role = ROLE_NAMES.ROLE_NAME_ADMIN + } else if (user.role === ROLE_NAMES.ROLE_NAME_MODERATOR) { + dbUser.userRoles[0].role = ROLE_NAMES.ROLE_NAME_MODERATOR + } + dbUser.userRoles[0].userId = dbUser.id + await dbUser.userRoles[0].save() } await dbUser.save() } diff --git a/backend/src/seeds/users/UserInterface.ts b/backend/src/seeds/users/UserInterface.ts index 08aa5d19d..5f441c009 100644 --- a/backend/src/seeds/users/UserInterface.ts +++ b/backend/src/seeds/users/UserInterface.ts @@ -8,5 +8,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..1bf9711b6 100644 --- a/backend/src/seeds/users/peter-lustig.ts +++ b/backend/src/seeds/users/peter-lustig.ts @@ -1,3 +1,5 @@ +import { ROLE_NAMES } from '@/auth/ROLES' + 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: ROLE_NAMES.ROLE_NAME_ADMIN, } diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index a8fab134f..d7c46df36 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: '', - userRole: undefined, + userRoles: undefined, publisherId: 0, // default password encryption type passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD,