diff --git a/apps/backend/src/routes/users/route.ts b/apps/backend/src/routes/users/route.ts index 3069960..6de294e 100644 --- a/apps/backend/src/routes/users/route.ts +++ b/apps/backend/src/routes/users/route.ts @@ -1,4 +1,4 @@ -import { and, eq, ilike, isNull, or, sql } from "drizzle-orm"; +import { and, eq, ilike, isNull, or, sql, not } from "drizzle-orm"; import { Hono } from "hono"; import { z } from "zod"; @@ -12,12 +12,19 @@ import HonoEnv from "../../types/HonoEnv"; import requestValidator from "../../utils/requestValidator"; import authInfo from "../../middlewares/authInfo"; import checkPermission from "../../middlewares/checkPermission"; +import { respondents } from "../../drizzle/schema/respondents"; +import { notFound } from "../../errors/DashboardError"; export const userFormSchema = z.object({ name: z.string().min(1).max(255), username: z.string().min(1).max(255), email: z.string().email().optional().or(z.literal("")), password: z.string().min(6), + companyName: z.string().min(1).max(255), + position: z.string().min(1).max(255), + workExperience: z.string().min(1).max(255), + address: z.string().min(1), + phoneNumber: z.string().min(1).max(13), isEnabled: z.string().default("false"), roles: z .string() @@ -87,9 +94,14 @@ const usersRoute = new Hono() createdAt: users.createdAt, updatedAt: users.updatedAt, ...(includeTrashed ? { deletedAt: users.deletedAt } : {}), + company: respondents.companyName, + roles: rolesSchema.name, fullCount: totalCountQuery, }) .from(users) + .leftJoin(respondents, eq(users.id, respondents.userId)) + .leftJoin(rolesToUsers, eq(users.id, rolesToUsers.userId)) + .leftJoin(rolesSchema, eq(rolesToUsers.roleId, rolesSchema.id)) .where( and( includeTrashed ? undefined : isNull(users.deletedAt), @@ -139,7 +151,12 @@ const usersRoute = new Hono() .select({ id: users.id, name: users.name, + position: respondents.position, + workExperience: respondents.workExperience, email: users.email, + companyName: respondents.companyName, + address: respondents.address, + phoneNumber: respondents.phoneNumber, username: users.username, isEnabled: users.isEnabled, createdAt: users.createdAt, @@ -151,6 +168,7 @@ const usersRoute = new Hono() }, }) .from(users) + .leftJoin(respondents, eq(users.id, respondents.userId)) .leftJoin(rolesToUsers, eq(users.id, rolesToUsers.userId)) .leftJoin(rolesSchema, eq(rolesToUsers.roleId, rolesSchema.id)) .where( @@ -184,34 +202,124 @@ const usersRoute = new Hono() .post( "/", checkPermission("users.create"), - requestValidator("form", userFormSchema), + requestValidator("json", userFormSchema), async (c) => { - const userData = c.req.valid("form"); + const userData = c.req.valid("json"); - const user = await db - .insert(users) - .values({ - name: userData.name, - username: userData.username, - email: userData.email, - password: await hashPassword(userData.password), - isEnabled: userData.isEnabled.toLowerCase() === "true", - }) - .returning(); - - if (userData.roles) { - const roles = JSON.parse(userData.roles) as string[]; - console.log(roles); - - if (roles.length) { - await db.insert(rolesToUsers).values( - roles.map((role) => ({ - userId: user[0].id, - roleId: role, - })) - ); - } + // Check if the provided email or username is already exists in database + const conditions = []; + if (userData.email) { + conditions.push(eq(users.email, userData.email)); } + conditions.push(eq(users.username, userData.username)); + + const existingUser = await db + .select() + .from(users) + .where( + or( + eq(users.email, userData.email), + eq(users.username, userData.username) + ) + ); + + const existingRespondent = await db + .select() + .from(respondents) + .where(eq(respondents.phoneNumber, userData.phoneNumber)); + + if (existingUser.length > 0) { + throw new HTTPException(400, { + message: "Email or username has been registered", + }); + } + + if (existingRespondent.length > 0) { + throw new HTTPException(400, { + message: "Phone number has been registered", + }); + } + + // Hash the password + const hashedPassword = await hashPassword(userData.password); + + // Start a transaction + const result = await db.transaction(async (trx) => { + // Create user + const [newUser] = await trx + .insert(users) + .values({ + name: userData.name, + username: userData.username, + email: userData.email, + password: hashedPassword, + isEnabled: userData.isEnabled?.toLowerCase() === "true" || true, + }) + .returning() + .catch(() => { + throw new HTTPException(500, { message: "Error creating user" }); + }); + + // Create respondent + const [newRespondent] = await trx + .insert(respondents) + .values({ + companyName: userData.companyName, + position: userData.position, + workExperience: userData.workExperience, + address: userData.address, + phoneNumber: userData.phoneNumber, + userId: newUser.id, + }) + .returning() + .catch((err) => { + throw new HTTPException(500, { + message: "Error creating respondent: " + err.message, + }); + }); + + // Automatically assign "user" role to the new user + const [role] = await trx + .select() + .from(rolesSchema) + .where(eq(rolesSchema.code, "user")) + .limit(1); + + if (!role) throw notFound(); + + await trx.insert(rolesToUsers).values({ + userId: newUser.id, + roleId: role.id, + }); + + // Add other roles if provided + if (userData.roles) { + const roles = JSON.parse(userData.roles) as string[]; + + for (let roleCode of roles) { + const role = ( + await trx + .select() + .from(rolesSchema) + .where(eq(rolesSchema.code, roleCode)) + .limit(1) + )[0]; + + if (role) { + await trx.insert(rolesToUsers).values({ + userId: newUser.id, + roleId: role.id, + }); + } else { + throw new HTTPException(404, { + message: `Role ${roleCode} does not exists`, + }); + } + } + } + + return newUser; + }); return c.json( { @@ -226,10 +334,32 @@ const usersRoute = new Hono() .patch( "/:id", checkPermission("users.update"), - requestValidator("form", userUpdateSchema), + requestValidator("json", userUpdateSchema), async (c) => { const userId = c.req.param("id"); - const userData = c.req.valid("form"); + const userData = c.req.valid("json"); + + // Check if the provided email or username is already exists in the database (excluding the current user) + if (userData.email || userData.username) { + const existingUser = await db + .select() + .from(users) + .where( + and( + or( + eq(users.email, userData.email), + eq(users.username, userData.username) + ), + not(eq(users.id, userId)) + ) + ); + + if (existingUser.length > 0) { + throw new HTTPException(400, { + message: "Email or username has been registered by another user", + }); + } + } const user = await db .select() @@ -238,18 +368,67 @@ const usersRoute = new Hono() if (!user[0]) return c.notFound(); - await db - .update(users) - .set({ - ...userData, - ...(userData.password - ? { password: await hashPassword(userData.password) } - : {}), - updatedAt: new Date(), - isEnabled: userData.isEnabled.toLowerCase() === "true", - }) - .where(eq(users.id, userId)); + // Start transaction to update both user and respondent + await db.transaction(async (trx) => { + // Update user + await trx + .update(users) + .set({ + ...userData, + ...(userData.password + ? { password: await hashPassword(userData.password) } + : {}), + updatedAt: new Date(), + isEnabled: userData.isEnabled.toLowerCase() === "true", + }) + .where(eq(users.id, userId)); + // Update respondent data if provided + if (userData.companyName || userData.position || userData.workExperience || userData.address || userData.phoneNumber) { + await trx + .update(respondents) + .set({ + ...(userData.companyName ? {companyName: userData.companyName} : {}), + ...(userData.position ? {position: userData.position} : {}), + ...(userData.workExperience ? {workExperience: userData.workExperience} : {}), + ...(userData.address ? {address: userData.address} : {}), + ...(userData.phoneNumber ? {phoneNumber: userData.phoneNumber} : {}), + updatedAt: new Date(), + }) + .where(eq(respondents.userId, userId)); + } + + // Update roles if provided + if (userData.roles) { + const roles = JSON.parse(userData.roles) as string[]; + + // Remove existing roles for the user + await trx.delete(rolesToUsers).where(eq(rolesToUsers.userId, userId)); + + // Assign new roles + for (let roleCode of roles) { + const role = ( + await trx + .select() + .from(rolesSchema) + .where(eq(rolesSchema.code, roleCode)) + .limit(1) + )[0]; + + if (role) { + await trx.insert(rolesToUsers).values({ + userId: userId, + roleId: role.id, + }); + } else { + throw new HTTPException(404, { + message: `Role ${roleCode} does not exist`, + }); + } + } + } + }); + return c.json({ message: "User updated successfully", }); @@ -273,6 +452,7 @@ const usersRoute = new Hono() const skipTrash = c.req.valid("form").skipTrash.toLowerCase() === "true"; + // Check if the user exists const user = await db .select() .from(users) @@ -283,17 +463,20 @@ const usersRoute = new Hono() ) ); + // Throw error if the user does not exist if (!user[0]) throw new HTTPException(404, { message: "The user is not found", }); + // Throw error if the user is trying to delete themselves if (user[0].id === currentUserId) { throw new HTTPException(400, { message: "You cannot delete yourself", }); } + // Delete or soft delete user if (skipTrash) { await db.delete(users).where(eq(users.id, userId)); } else { @@ -311,28 +494,34 @@ const usersRoute = new Hono() ) //undo delete - .patch("/restore/:id", checkPermission("users.restore"), async (c) => { - const userId = c.req.param("id"); + .patch( + "/restore/:id", + checkPermission("users.restore"), + async (c) => { + const userId = c.req.param("id"); - const user = ( - await db.select().from(users).where(eq(users.id, userId)) - )[0]; + // Check if the user exists + const user = ( + await db.select().from(users).where(eq(users.id, userId)) + )[0]; - if (!user) return c.notFound(); + if (!user) return c.notFound(); - if (!user.deletedAt) { - throw new HTTPException(400, { - message: "The user is not deleted", + // Throw error if the user is not deleted + if (!user.deletedAt) { + throw new HTTPException(400, { + message: "The user is not deleted", + }); + } + + // Restore user + await db + .update(users) + .set({ deletedAt: null }) + .where(eq(users.id, userId)); + + return c.json({ + message: "User restored successfully", }); - } - - await db - .update(users) - .set({ deletedAt: null }) - .where(eq(users.id, userId)); - - return c.json({ - message: "User restored successfully", - }); }); export default usersRoute;