diff --git a/client/src/Pages/Account/components/AddTeamMemberDialog.tsx b/client/src/Pages/Account/components/AddTeamMemberDialog.tsx index aed713b1f..170af1724 100644 --- a/client/src/Pages/Account/components/AddTeamMemberDialog.tsx +++ b/client/src/Pages/Account/components/AddTeamMemberDialog.tsx @@ -17,7 +17,11 @@ interface AddTeamMemberDialogProps { } interface RegisterPayload { - user: Omit; + firstName: string; + lastName: string; + email: string; + password: string; + role: string[]; } export const AddTeamMemberDialog = ({ @@ -50,9 +54,9 @@ export const AddTeamMemberDialog = ({ if (loading) return; const { confirm, ...userData } = data; - const payload: RegisterPayload = { user: userData }; + const payload: RegisterPayload = userData; - const result = await post("/auth/register", payload); + const result = await post("/auth/users", payload); if (result?.success) { reset(); diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts index d6fe6aeff..3fa551808 100755 --- a/server/src/controllers/authController.ts +++ b/server/src/controllers/authController.ts @@ -1,10 +1,13 @@ import { Request, Response, NextFunction } from "express"; import { AppError } from "@/utils/AppError.js"; +import { requireTeamId, requireUserRoles } from "./controllerUtils.js"; +import type { UserRole } from "@/types/user.js"; import { registrationBodyValidation, loginValidation, editUserBodyValidation, + createUserBodyValidation, recoveryValidation, recoveryTokenBodyValidation, newPasswordValidation, @@ -50,6 +53,27 @@ class AuthController { } }; + createUser = async (req: Request, res: Response, next: NextFunction) => { + try { + const userData = req.body; + if (userData?.email) { + userData.email = userData.email.toLowerCase(); + } + await createUserBodyValidation.validateAsync(userData); + + const teamId = requireTeamId(req.user?.teamId); + const actorRoles = requireUserRoles(req.user?.role) as UserRole[]; + const newUser = await this.userService.createUser(userData, teamId, actorRoles, req.file); + res.status(201).json({ + success: true, + msg: "User created successfully", + data: newUser, + }); + } catch (error) { + next(error); + } + }; + loginUser = async (req: Request, res: Response, next: NextFunction) => { try { if (req.body?.email) { diff --git a/server/src/controllers/inviteController.ts b/server/src/controllers/inviteController.ts index e6d5d7ccb..bba04fbbd 100755 --- a/server/src/controllers/inviteController.ts +++ b/server/src/controllers/inviteController.ts @@ -15,7 +15,6 @@ class InviteController { } getInviteToken = async (req: Request, res: Response, next: NextFunction) => { - console.log(req.body); try { const teamId = requireTeamId(req.user?.teamId); const userRoles = requireUserRoles(req.user?.role); diff --git a/server/src/routes/authRoute.ts b/server/src/routes/authRoute.ts index 97e85d61f..cc1ec3fd6 100755 --- a/server/src/routes/authRoute.ts +++ b/server/src/routes/authRoute.ts @@ -25,6 +25,7 @@ class AuthRoutes { this.router.get("/users/superadmin", this.authController.checkSuperadminExists); this.router.get("/users", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.getAllUsers); + this.router.post("/users", verifyJWT, isAllowed(["superadmin"]), upload.single("profileImage"), this.authController.createUser); this.router.get("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.getUserById); this.router.patch("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserById); this.router.patch("/users/:userId/password", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserPasswordById); diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index 39b05a737..d05f027b1 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -7,6 +7,7 @@ import { ITeamsRepository, } from "@/repositories/index.js"; import type { User } from "@/types/index.js"; +import { canManageRole, type UserRole } from "@/types/user.js"; import bcrypt from "bcryptjs"; import { AppError } from "@/utils/AppError.js"; import { ISuperSimpleQueue } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; @@ -153,6 +154,43 @@ class UserService { return { user: newUser, token }; }; + createUser = async (userData: Partial, teamId: string, actorRoles: UserRole[], file: any) => { + // Validate that the creator can assign the requested roles + const targetRoles = userData.role ?? []; + for (const targetRole of targetRoles) { + const canManage = actorRoles.some((actorRole) => canManageRole(actorRole, targetRole)); + if (!canManage) { + throw new AppError({ + message: "You do not have permission to assign this role", + service: SERVICE_NAME, + method: "createUser", + status: 403, + }); + } + } + + userData.teamId = teamId; + + if (userData.password) { + userData.password = this.hashPassword(userData.password); + } + + const newUser = await this.usersRepository.create(userData, file); + + this.logger.debug({ + message: "New user created by superadmin", + service: SERVICE_NAME, + method: "createUser", + details: newUser.id, + }); + + newUser.profileImage = undefined; + newUser.avatarImage = undefined; + newUser.password = ""; + + return newUser; + }; + loginUser = async (email: string, password: string) => { // Check if user exists const user = await this.usersRepository.findByEmail(email); diff --git a/server/src/validation/joi.ts b/server/src/validation/joi.ts index f72addc5e..f01a8da40 100755 --- a/server/src/validation/joi.ts +++ b/server/src/validation/joi.ts @@ -712,6 +712,28 @@ const editUserPasswordByIdBodyValidation = joi.object({ password: joi.string().min(8).required().pattern(passwordPattern), }); +const createUserBodyValidation = joi.object({ + firstName: nameValidation.required(), + lastName: nameValidation.required(), + email: joi + .string() + .email() + .required() + .custom((value, helpers) => { + const lowercasedValue = value.toLowerCase(); + if (value !== lowercasedValue) { + return helpers.message({ custom: "Email must be in lowercase" }); + } + return lowercasedValue; + }), + password: joi.string().min(8).required().pattern(passwordPattern), + role: joi + .array() + .items(joi.string().valid(...UserRoles)) + .min(1) + .required(), +}); + export { roleValidatior, loginValidation, @@ -781,4 +803,5 @@ export { editUserByIdBodyValidation, editSuperadminUserByIdBodyValidation, editUserPasswordByIdBodyValidation, + createUserBodyValidation, };