From de0674d82cab57e4e6106044e7cd6361d4a25b0b Mon Sep 17 00:00:00 2001 From: juancarmore Date: Thu, 22 Jan 2026 11:38:40 +0100 Subject: [PATCH] backend: implement user role update functionality --- .../src/controllers/user.controller.ts | 22 +++++++++++- .../user-validator.middleware.ts | 12 +++++++ meet-ce/backend/src/models/error.model.ts | 16 +++++++++ .../src/models/zod-schemas/user.schema.ts | 4 +++ meet-ce/backend/src/routes/user.routes.ts | 9 ++++- meet-ce/backend/src/services/user.service.ts | 34 +++++++++++++++++++ 6 files changed, 95 insertions(+), 2 deletions(-) diff --git a/meet-ce/backend/src/controllers/user.controller.ts b/meet-ce/backend/src/controllers/user.controller.ts index 91e4e829..5e2742b6 100644 --- a/meet-ce/backend/src/controllers/user.controller.ts +++ b/meet-ce/backend/src/controllers/user.controller.ts @@ -1,4 +1,4 @@ -import { MeetUserFilters, MeetUserOptions } from '@openvidu-meet/typings'; +import { MeetUserFilters, MeetUserOptions, MeetUserRole } from '@openvidu-meet/typings'; import { Request, Response } from 'express'; import { container } from '../config/dependency-injector.config.js'; import { INTERNAL_CONFIG } from '../config/internal-config.js'; @@ -150,6 +150,26 @@ export const resetUserPassword = async (req: Request, res: Response) => { } }; +export const updateUserRole = async (req: Request, res: Response) => { + const { userId } = req.params; + const { role } = req.body as { role: MeetUserRole }; + + const logger = container.get(LoggerService); + logger.verbose(`Admin updating role for user '${userId}' to '${role}'`); + + try { + const userService = container.get(UserService); + const user = await userService.changeUserRole(userId, role); + + return res.status(200).json({ + message: `Role for user '${userId}' updated successfully to '${role}'`, + user: userService.convertToDTO(user) + }); + } catch (error) { + handleError(res, error, 'updating user role'); + } +}; + export const changePassword = async (req: Request, res: Response) => { const requestSessionService = container.get(RequestSessionService); const user = requestSessionService.getAuthenticatedUser(); diff --git a/meet-ce/backend/src/middlewares/request-validators/user-validator.middleware.ts b/meet-ce/backend/src/middlewares/request-validators/user-validator.middleware.ts index 26b67354..0134fad4 100644 --- a/meet-ce/backend/src/middlewares/request-validators/user-validator.middleware.ts +++ b/meet-ce/backend/src/middlewares/request-validators/user-validator.middleware.ts @@ -4,6 +4,7 @@ import { BulkDeleteUsersReqSchema, ChangePasswordReqSchema, ResetUserPasswordReqSchema, + UpdateUserRoleReqSchema, UserFiltersSchema, UserOptionsSchema } from '../../models/zod-schemas/user.schema.js'; @@ -65,3 +66,14 @@ export const validateResetUserPasswordReq = (req: Request, res: Response, next: req.body = data; next(); }; + +export const validateUpdateUserRoleReq = (req: Request, res: Response, next: NextFunction) => { + const { success, error, data } = UpdateUserRoleReqSchema.safeParse(req.body); + + if (!success) { + return rejectUnprocessableRequest(res, error); + } + + req.body = data; + next(); +}; diff --git a/meet-ce/backend/src/models/error.model.ts b/meet-ce/backend/src/models/error.model.ts index 77f49dde..d06346d7 100644 --- a/meet-ce/backend/src/models/error.model.ts +++ b/meet-ce/backend/src/models/error.model.ts @@ -249,6 +249,22 @@ export const errorCannotDeleteOwnAccount = (): OpenViduMeetError => { ); }; +export const errorCannotChangeRootAdminRole = (): OpenViduMeetError => { + return new OpenViduMeetError( + 'User Error', + 'Cannot change the role of the root admin user. This account must retain administrative privileges.', + 403 + ); +}; + +export const errorCannotChangeOwnRole = (): OpenViduMeetError => { + return new OpenViduMeetError( + 'User Error', + 'Cannot change your own role. Please have another administrator change your role if needed.', + 403 + ); +}; + // Room errors export const errorRoomNotFound = (roomId: string): OpenViduMeetError => { diff --git a/meet-ce/backend/src/models/zod-schemas/user.schema.ts b/meet-ce/backend/src/models/zod-schemas/user.schema.ts index 27506d8f..f854b198 100644 --- a/meet-ce/backend/src/models/zod-schemas/user.schema.ts +++ b/meet-ce/backend/src/models/zod-schemas/user.schema.ts @@ -58,3 +58,7 @@ export const ChangePasswordReqSchema = z.object({ export const ResetUserPasswordReqSchema = z.object({ newPassword: z.string().min(5, 'New password must be at least 5 characters long') }); + +export const UpdateUserRoleReqSchema = z.object({ + role: z.nativeEnum(MeetUserRole) +}); diff --git a/meet-ce/backend/src/routes/user.routes.ts b/meet-ce/backend/src/routes/user.routes.ts index ff18fe3f..956c2992 100644 --- a/meet-ce/backend/src/routes/user.routes.ts +++ b/meet-ce/backend/src/routes/user.routes.ts @@ -8,7 +8,8 @@ import { validateChangePasswordReq, validateCreateUserReq, validateGetUsersReq, - validateResetUserPasswordReq + validateResetUserPasswordReq, + validateUpdateUserRoleReq } from '../middlewares/request-validators/user-validator.middleware.js'; export const userRouter: Router = Router(); @@ -49,4 +50,10 @@ userRouter.put( validateResetUserPasswordReq, userCtrl.resetUserPassword ); +userRouter.put( + '/:userId/role', + withAuth(tokenAndRoleValidator(MeetUserRole.ADMIN)), + validateUpdateUserRoleReq, + userCtrl.updateUserRole +); userRouter.delete('/:userId', withAuth(tokenAndRoleValidator(MeetUserRole.ADMIN)), userCtrl.deleteUser); diff --git a/meet-ce/backend/src/services/user.service.ts b/meet-ce/backend/src/services/user.service.ts index 1ff77bd4..3539131c 100644 --- a/meet-ce/backend/src/services/user.service.ts +++ b/meet-ce/backend/src/services/user.service.ts @@ -3,6 +3,8 @@ import { inject, injectable } from 'inversify'; import { MEET_ENV } from '../environment.js'; import { PasswordHelper } from '../helpers/password.helper.js'; import { + errorCannotChangeOwnRole, + errorCannotChangeRootAdminRole, errorCannotDeleteOwnAccount, errorCannotDeleteRootAdmin, errorCannotResetOwnPassword, @@ -151,6 +153,38 @@ export class UserService { this.logger.info(`Password reset for user '${userId}' by admin. User must change password on next login.`); } + /** + * Change user role by admin. + * + * @param userId - The ID of the user whose role will be changed + * @param newRole - The new role to assign to the user + */ + async changeUserRole(userId: string, newRole: MeetUserRole): Promise { + // Prevent changing role of the root admin user + if (userId === MEET_ENV.INITIAL_ADMIN_USER) { + throw errorCannotChangeRootAdminRole(); + } + + // Prevent changing own role + const authenticatedUser = this.requestSessionService.getAuthenticatedUser(); + + if (authenticatedUser && authenticatedUser.userId === userId) { + throw errorCannotChangeOwnRole(); + } + + const user = await this.userRepository.findByUserId(userId); + + if (!user) { + throw errorUserNotFound(userId); + } + + user.role = newRole; + await this.userRepository.update(user); + + this.logger.info(`Role for user '${userId}' changed to '${newRole}' by admin`); + return user; + } + async deleteUser(userId: string): Promise { // Prevent deleting the root admin user if (userId === MEET_ENV.INITIAL_ADMIN_USER) {