From d2ecdfab11b9cefa8bf2d6e7e5915e1947ca3597 Mon Sep 17 00:00:00 2001 From: abiyasa05 Date: Thu, 8 Aug 2024 14:28:24 +0700 Subject: [PATCH 01/12] create: API for register --- apps/backend/src/data/permissions.ts | 3 + apps/backend/src/data/roles.ts | 10 ++- apps/backend/src/index.ts | 2 + apps/backend/src/routes/register/route.ts | 83 +++++++++++++++++++++++ 4 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 apps/backend/src/routes/register/route.ts diff --git a/apps/backend/src/data/permissions.ts b/apps/backend/src/data/permissions.ts index 1e91356..8fc61fe 100644 --- a/apps/backend/src/data/permissions.ts +++ b/apps/backend/src/data/permissions.ts @@ -32,6 +32,9 @@ const permissionsData = [ { code: "roles.delete", }, + { + code: "register.create", + }, ] as const; export type SpecificPermissionCode = (typeof permissionsData)[number]["code"]; diff --git a/apps/backend/src/data/roles.ts b/apps/backend/src/data/roles.ts index 2c42327..bc087b5 100644 --- a/apps/backend/src/data/roles.ts +++ b/apps/backend/src/data/roles.ts @@ -17,10 +17,18 @@ const roleData: RoleData[] = [ name: "Super Admin", permissions: permissionsData.map((permission) => permission.code), }, + { + code: "user", + description: + "Has full access to the system and can manage all features and settings", + isActive: true, + name: "User", + permissions: ["register.create"], + }, ]; // Manually specify the union of role codes -export type RoleCode = "super-admin" | "*"; +export type RoleCode = "super-admin" | "user" | "*"; const exportedRoleData = roleData; diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index c0fbe56..a8204c1 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -3,6 +3,7 @@ import { configDotenv } from "dotenv"; import { Hono } from "hono"; import authRoutes from "./routes/auth/route"; import usersRoute from "./routes/users/route"; +import respondentsRoute from "./routes/register/route"; import { verifyAccessToken } from "./utils/authUtils"; import permissionRoutes from "./routes/permissions/route"; import { cors } from "hono/cors"; @@ -78,6 +79,7 @@ const routes = app .route("/dashboard", dashboardRoutes) .route("/roles", rolesRoute) .route("/dev", devRoutes) + .route("/register", respondentsRoute) .onError((err, c) => { if (err instanceof DashboardError) { return c.json( diff --git a/apps/backend/src/routes/register/route.ts b/apps/backend/src/routes/register/route.ts new file mode 100644 index 0000000..9ac5706 --- /dev/null +++ b/apps/backend/src/routes/register/route.ts @@ -0,0 +1,83 @@ +import { Hono } from "hono"; +import { HTTPException } from "hono/http-exception"; +import db from "../../drizzle"; +import { respondents } from "../../drizzle/schema/respondents"; +import { users } from "../../drizzle/schema/users"; +import { hashPassword } from "../../utils/passwordUtils"; +import requestValidator from "../../utils/requestValidator"; +import authInfo from "../../middlewares/authInfo"; +import checkPermission from "../../middlewares/checkPermission"; +import { and, eq, isNull, ilike, or, sql } from "drizzle-orm"; +import { z } from "zod"; +import HonoEnv from "../../types/HonoEnv"; + +const registerFormSchema = z.object({ + name: z.string().min(1).max(255), + username: z.string().min(1).max(255), + email: z.string().email().optional(), + 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), +}); + +const respondentsRoute = new Hono() + .use(authInfo) + //create user and respondent + .post( + "/", + checkPermission("register.create"), + requestValidator("json", registerFormSchema), + async (c) => { + const formData = c.req.valid("json"); + + // Hash the password + const hashedPassword = await hashPassword(formData.password); + + // Start a transaction + try { + const result = await db.transaction(async (trx) => { + // Create user + const [newUser] = await trx + .insert(users) + .values({ + name: formData.name, + username: formData.username, + email: formData.email, + password: hashedPassword, + }) + .returning(); + + // Create respondent + await trx + .insert(respondents) + .values({ + companyName: formData.companyName, + position: formData.position, + workExperience: formData.workExperience, + address: formData.address, + phoneNumber: formData.phoneNumber, + userId: newUser.id, + }); + + return newUser; + }); + + return c.json( + { + message: "User and respondent created successfully", + }, + 201 + ); + } catch (error) { + console.error("Error creating user and respondent:", error); + throw new HTTPException(500, { + message: "Error creating user and respondent", + }); + } + } + ); + +export default respondentsRoute; \ No newline at end of file From 34abe4582dded63c139b94bfd06399eca34c7e0c Mon Sep 17 00:00:00 2001 From: abiyasa05 Date: Fri, 9 Aug 2024 09:21:27 +0700 Subject: [PATCH 02/12] add: data validation for register api --- apps/backend/src/routes/register/route.ts | 32 +++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/routes/register/route.ts b/apps/backend/src/routes/register/route.ts index 9ac5706..432637c 100644 --- a/apps/backend/src/routes/register/route.ts +++ b/apps/backend/src/routes/register/route.ts @@ -7,7 +7,7 @@ import { hashPassword } from "../../utils/passwordUtils"; import requestValidator from "../../utils/requestValidator"; import authInfo from "../../middlewares/authInfo"; import checkPermission from "../../middlewares/checkPermission"; -import { and, eq, isNull, ilike, or, sql } from "drizzle-orm"; +import { and, eq, or } from "drizzle-orm"; import { z } from "zod"; import HonoEnv from "../../types/HonoEnv"; @@ -25,7 +25,6 @@ const registerFormSchema = z.object({ const respondentsRoute = new Hono() .use(authInfo) - //create user and respondent .post( "/", checkPermission("register.create"), @@ -33,6 +32,35 @@ const respondentsRoute = new Hono() async (c) => { const formData = c.req.valid("json"); + // Build conditions based on available formData + const conditions = []; + if (formData.email) { + conditions.push(eq(users.email, formData.email)); + } + conditions.push(eq(users.username, formData.username)); + + const existingUser = await db + .select() + .from(users) + .where(or(...conditions)); + + const existingRespondent = await db + .select() + .from(respondents) + .where(eq(respondents.phoneNumber, formData.phoneNumber)); + + if (existingUser.length > 0) { + throw new HTTPException(400, { + message: "Email atau username sudah terdaftar", + }); + } + + if (existingRespondent.length > 0) { + throw new HTTPException(400, { + message: "Nomor HP sudah terdaftar", + }); + } + // Hash the password const hashedPassword = await hashPassword(formData.password); From 70702950d4f57b862197c6fa9c8a37400f5aa913 Mon Sep 17 00:00:00 2001 From: percyfikri Date: Fri, 9 Aug 2024 10:26:06 +0700 Subject: [PATCH 03/12] Create : API for management-aspect --- apps/backend/src/index.ts | 2 + .../src/routes/managementAspect/route.ts | 418 ++++++++++++++++++ 2 files changed, 420 insertions(+) create mode 100644 apps/backend/src/routes/managementAspect/route.ts diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index c0fbe56..252a875 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -3,6 +3,7 @@ import { configDotenv } from "dotenv"; import { Hono } from "hono"; import authRoutes from "./routes/auth/route"; import usersRoute from "./routes/users/route"; +import aspectsRoute from "./routes/managementAspect/route"; import { verifyAccessToken } from "./utils/authUtils"; import permissionRoutes from "./routes/permissions/route"; import { cors } from "hono/cors"; @@ -78,6 +79,7 @@ const routes = app .route("/dashboard", dashboardRoutes) .route("/roles", rolesRoute) .route("/dev", devRoutes) + .route("/management-aspect", aspectsRoute) .onError((err, c) => { if (err instanceof DashboardError) { return c.json( diff --git a/apps/backend/src/routes/managementAspect/route.ts b/apps/backend/src/routes/managementAspect/route.ts new file mode 100644 index 0000000..80cf70e --- /dev/null +++ b/apps/backend/src/routes/managementAspect/route.ts @@ -0,0 +1,418 @@ +import { and, eq, ilike, isNull, or, sql } from "drizzle-orm"; +import { Hono } from "hono"; + +import { z } from "zod"; +import { HTTPException } from "hono/http-exception"; +import db from "../../drizzle"; +import { aspects } from "../../drizzle/schema/aspects"; +import { subAspects } from "../../drizzle/schema/subAspects"; +import HonoEnv from "../../types/HonoEnv"; +import requestValidator from "../../utils/requestValidator"; +import authInfo from "../../middlewares/authInfo"; +import checkPermission from "../../middlewares/checkPermission"; + +// Schema for creating and updating aspects +export const aspectFormSchema = z.object({ + name: z.string().min(1).max(50), + subAspects: z + .string() + .refine( + (data) => { + try { + const parsed = JSON.parse(data); + return Array.isArray(parsed); + } catch { + return false; + } + }, + { + message: "Sub Aspects must be an array", + } + ) + .optional(), +}); + +export const aspectUpdateSchema = aspectFormSchema.extend({ + subAspects: z.string().optional().or(z.literal("")), +}); + +// Schema for creating and updating subAspects +export const subAspectFormSchema = z.object({ + name: z.string().min(1).max(50), + aspectId: z.string().uuid(), +}); + +export const subAspectUpdateSchema = subAspectFormSchema.extend({}); + +const managementAspectRoute = new Hono() + .use(authInfo) + /** + * Get All Aspects (With Metadata) + * + * Query params: + * - includeTrashed: boolean (default: false) + * - withMetadata: boolean + */ + // Get all aspects + .get( + "/", + requestValidator( + "query", + z.object({ + includeTrashed: z + .string() + .optional() + .transform((v) => v?.toLowerCase() === "true"), + withMetadata: z + .string() + .optional() + .transform((v) => v?.toLowerCase() === "true"), + page: z.coerce.number().int().min(0).default(0), + limit: z.coerce.number().int().min(1).max(1000).default(10), + q: z.string().default(""), + }) + ), + async (c) => { + const { includeTrashed, page, limit, q } = c.req.valid("query"); + + const totalCountQuery = includeTrashed + ? sql`(SELECT count(*) FROM ${aspects})` + : sql`(SELECT count(*) FROM ${aspects} WHERE ${aspects.deletedAt} IS NULL)`; + + const result = await db + .select({ + id: aspects.id, + name: aspects.name, + createdAt: aspects.createdAt, + updatedAt: aspects.updatedAt, + ...(includeTrashed ? { deletedAt: aspects.deletedAt } : {}), + fullCount: totalCountQuery, + }) + .from(aspects) + .where( + and( + includeTrashed ? undefined : isNull(aspects.deletedAt), + q ? or(ilike(aspects.name, q), eq(aspects.id, q)) : undefined + ) + ) + .offset(page * limit) + .limit(limit); + + return c.json({ + data: result.map((d) => ({ ...d, fullCount: undefined })), + _metadata: { + currentPage: page, + totalPages: Math.ceil((Number(result[0]?.fullCount) ?? 0) / limit), + totalItems: Number(result[0]?.fullCount) ?? 0, + perPage: limit, + }, + }); + } + ) + // Get aspect by id + .get( + "/:id", + requestValidator( + "query", + z.object({ + includeTrashed: z.string().default("false"), + }) + ), + async (c) => { + const aspectId = c.req.param("id"); + + if (!aspectId) throw new HTTPException(400, { message: "Missing id" }); + + const includeTrashed = + c.req.query("includeTrashed")?.toLowerCase() === "true"; + + const queryResult = await db + .select({ + id: aspects.id, + name: aspects.name, + createdAt: aspects.createdAt, + updatedAt: aspects.updatedAt, + ...(includeTrashed ? { deletedAt: aspects.deletedAt } : {}), + subAspect: { + name: subAspects.name, + id: subAspects.id, + }, + }) + .from(aspects) + .leftJoin(subAspects, eq(aspects.id, subAspects.aspectId)) + .where( + and( + eq(aspects.id, aspectId), + !includeTrashed ? isNull(aspects.deletedAt) : undefined + ) + ); + + if (!queryResult.length) + throw new HTTPException(404, { + message: "The aspect does not exist", + }); + + const subAspectsList = queryResult.reduce((prev, curr) => { + if (!curr.subAspect) return prev; + prev.set(curr.subAspect.id, curr.subAspect.name); + return prev; + }, new Map()); // Map + + const aspectData = { + ...queryResult[0], + subAspect: undefined, + subAspects: Array.from(subAspectsList, ([id, name]) => ({ id, name })), + }; + + return c.json(aspectData); + } + ) + // Create aspect + .post( + "/", + requestValidator("json", aspectFormSchema), + async (c) => { + const aspectData = c.req.valid("json"); + + const aspect = await db + .insert(aspects) + .values({ + name: aspectData.name, + }) + .returning(); + + if (aspectData.subAspects) { + const subAspectsArray = JSON.parse(aspectData.subAspects) as string[]; + + if (subAspectsArray.length) { + await db.insert(subAspects).values( + subAspectsArray.map((subAspect) => ({ + aspectId: aspect[0].id, + name: subAspect, + })) + ); + } + } + + return c.json( + { + message: "Aspect created successfully", + }, + 201 + ); + } + ) + // Update aspect + .patch( + "/:id", + requestValidator("json", aspectUpdateSchema), + async (c) => { + const aspectId = c.req.param("id"); + const aspectData = c.req.valid("json"); + + const aspect = await db + .select() + .from(aspects) + .where(and(eq(aspects.id, aspectId), isNull(aspects.deletedAt))); + + if (!aspect[0]) return c.notFound(); + + await db + .update(aspects) + .set({ + ...aspectData, + updatedAt: new Date(), + }) + .where(eq(aspects.id, aspectId)); + + if (aspectData.subAspects) { + const subAspectsArray = JSON.parse(aspectData.subAspects) as string[]; + + await db.delete(subAspects).where(eq(subAspects.aspectId, aspectId)); + + if (subAspectsArray.length) { + await db.insert(subAspects).values( + subAspectsArray.map((subAspect) => ({ + aspectId: aspectId, + name: subAspect, + })) + ); + } + } + + return c.json({ + message: "Aspect updated successfully", + }); + } + ) + // Delete aspect + .delete( + "/:id", + requestValidator( + "form", + z.object({ + skipTrash: z.string().default("false"), + }) + ), + async (c) => { + const aspectId = c.req.param("id"); + + const skipTrash = c.req.valid("form").skipTrash.toLowerCase() === "true"; + + const aspect = await db + .select() + .from(aspects) + .where(and(eq(aspects.id, aspectId), skipTrash ? undefined : isNull(aspects.deletedAt))); + + if (!aspect[0]) + throw new HTTPException(404, { + message: "The aspect is not found", + }); + + if (skipTrash) { + await db.delete(aspects).where(eq(aspects.id, aspectId)); + } else { + await db + .update(aspects) + .set({ + deletedAt: new Date(), + }) + .where(and(eq(aspects.id, aspectId), isNull(aspects.deletedAt))); + } + return c.json({ + message: "Aspect deleted successfully", + }); + } + ) + // Undo delete + .patch( + "/restore/:id", + async (c) => { + const aspectId = c.req.param("id"); + + const aspect = (await db.select().from(aspects).where(eq(aspects.id, aspectId)))[0]; + + if (!aspect) return c.notFound(); + + if (!aspect.deletedAt) { + throw new HTTPException(400, { + message: "The aspect is not deleted", + }); + } + + await db.update(aspects).set({ deletedAt: null }).where(eq(aspects.id, aspectId)); + + return c.json({ + message: "Aspect restored successfully", + }); + } + ) + // Create sub aspect + .post( + "/subAspect", + requestValidator("json", subAspectFormSchema), + async (c) => { + const subAspectData = c.req.valid("json"); + + const aspect = await db + .select() + .from(aspects) + .where(and(eq(aspects.id, subAspectData.aspectId), isNull(aspects.deletedAt))); + + if (!aspect[0]) + throw new HTTPException(404, { + message: "The aspect is not found", + }); + + await db.insert(subAspects).values(subAspectData); + + return c.json( + { + message: "Sub aspect created successfully", + }, + 201 + ); + } + ) + // Update sub aspect + .patch( + "/subAspect/:id", + requestValidator("json", subAspectUpdateSchema), + async (c) => { + const subAspectId = c.req.param("id"); + const subAspectData = c.req.valid("json"); + + const subAspect = await db + .select() + .from(subAspects) + .where(eq(subAspects.id, subAspectId)); + + if (!subAspect[0]) + throw new HTTPException(404, { + message: "The sub aspect is not found", + }); + + await db + .update(subAspects) + .set({ + ...subAspectData, + updatedAt: new Date(), + }) + .where(eq(subAspects.id, subAspectId)); + + return c.json({ + message: "Sub aspect updated successfully", + }); + } + ) + // Delete sub aspect + .delete( + "/subAspect/:id", + async (c) => { + const subAspectId = c.req.param("id"); + + const subAspect = await db + .select() + .from(subAspects) + .where(eq(subAspects.id, subAspectId)); + + if (!subAspect[0]) + throw new HTTPException(404, { + message: "The sub aspect is not found", + }); + + await db.delete(subAspects).where(eq(subAspects.id, subAspectId)); + + return c.json({ + message: "Sub aspect deleted successfully", + }); + } + ) + // Get sub aspects by aspect ID + .get( + "/subAspects/:aspectId", + async (c) => { + const aspectId = c.req.param("aspectId"); + + const aspect = await db + .select() + .from(aspects) + .where(and(eq(aspects.id, aspectId), isNull(aspects.deletedAt))); + + if (!aspect[0]) + throw new HTTPException(404, { + message: "The aspect is not found", + }); + + const subAspectsData = await db + .select() + .from(subAspects) + .where(eq(subAspects.aspectId, aspectId)); + + return c.json({ + subAspects: subAspectsData, + }); + } + ); + +export default managementAspectRoute; From 46e8980941438a9fb313ef26a2e2f23cf75173f7 Mon Sep 17 00:00:00 2001 From: abiyasa05 Date: Fri, 9 Aug 2024 11:03:22 +0700 Subject: [PATCH 04/12] Add a role to the register API --- apps/backend/src/routes/register/route.ts | 98 +++++++++++++++++------ 1 file changed, 72 insertions(+), 26 deletions(-) diff --git a/apps/backend/src/routes/register/route.ts b/apps/backend/src/routes/register/route.ts index 432637c..a8fae90 100644 --- a/apps/backend/src/routes/register/route.ts +++ b/apps/backend/src/routes/register/route.ts @@ -3,6 +3,7 @@ import { HTTPException } from "hono/http-exception"; import db from "../../drizzle"; import { respondents } from "../../drizzle/schema/respondents"; import { users } from "../../drizzle/schema/users"; +import { rolesToUsers } from "../../drizzle/schema/rolesToUsers"; import { hashPassword } from "../../utils/passwordUtils"; import requestValidator from "../../utils/requestValidator"; import authInfo from "../../middlewares/authInfo"; @@ -21,6 +22,23 @@ const registerFormSchema = z.object({ 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() + .refine( + (data) => { + try { + const parsed = JSON.parse(data); + return Array.isArray(parsed); + } catch { + return false; + } + }, + { + message: "Roles must be an array", + } + ) + .optional(), }); const respondentsRoute = new Hono() @@ -31,39 +49,47 @@ const respondentsRoute = new Hono() requestValidator("json", registerFormSchema), async (c) => { const formData = c.req.valid("json"); - + + console.log("Form Data:", formData); + // Build conditions based on available formData const conditions = []; if (formData.email) { conditions.push(eq(users.email, formData.email)); } conditions.push(eq(users.username, formData.username)); - + const existingUser = await db .select() .from(users) .where(or(...conditions)); - + + console.log("Existing Users:", existingUser); + const existingRespondent = await db .select() .from(respondents) .where(eq(respondents.phoneNumber, formData.phoneNumber)); - + + console.log("Existing Respondents:", existingRespondent); + if (existingUser.length > 0) { throw new HTTPException(400, { message: "Email atau username sudah terdaftar", }); } - + if (existingRespondent.length > 0) { throw new HTTPException(400, { message: "Nomor HP sudah terdaftar", }); } - + // Hash the password const hashedPassword = await hashPassword(formData.password); - + + console.log("Hashed Password:", hashedPassword); + // Start a transaction try { const result = await db.transaction(async (trx) => { @@ -75,30 +101,50 @@ const respondentsRoute = new Hono() username: formData.username, email: formData.email, password: hashedPassword, + isEnabled: formData.isEnabled?.toLowerCase() === "true" || true, }) .returning(); - + + console.log("New User:", newUser); + // Create respondent - await trx - .insert(respondents) - .values({ - companyName: formData.companyName, - position: formData.position, - workExperience: formData.workExperience, - address: formData.address, - phoneNumber: formData.phoneNumber, - userId: newUser.id, - }); - + await trx.insert(respondents).values({ + companyName: formData.companyName, + position: formData.position, + workExperience: formData.workExperience, + address: formData.address, + phoneNumber: formData.phoneNumber, + userId: newUser.id, + }); + + console.log("Respondent Created for User ID:", newUser.id); + + // If roles are included in the request, add to rolesToUsers + if (formData.roles) { + try { + const roles = JSON.parse(formData.roles) as string[]; + if (roles.length) { + await trx.insert(rolesToUsers).values( + roles.map((role) => ({ + userId: newUser.id, + roleId: role, + })) + ); + } + } catch (error) { + console.error("Error parsing roles:", error); + throw new HTTPException(400, { + message: "Invalid roles format", + }); + } + } + return newUser; }); - - return c.json( - { - message: "User and respondent created successfully", - }, - 201 - ); + + return c.json({ + message: "User and respondent created successfully", + }, 201); } catch (error) { console.error("Error creating user and respondent:", error); throw new HTTPException(500, { From 94cf958318f62c07b67cb8e653a69f00a0810cfa Mon Sep 17 00:00:00 2001 From: percyfikri Date: Mon, 12 Aug 2024 11:33:56 +0700 Subject: [PATCH 05/12] update : add checkpermission and added api merge for aspects and subAspects --- apps/backend/src/data/permissions.ts | 15 + apps/backend/src/index.ts | 4 +- .../src/routes/managementAspect/route.ts | 827 ++++++++++-------- 3 files changed, 469 insertions(+), 377 deletions(-) diff --git a/apps/backend/src/data/permissions.ts b/apps/backend/src/data/permissions.ts index 1e91356..9c18eca 100644 --- a/apps/backend/src/data/permissions.ts +++ b/apps/backend/src/data/permissions.ts @@ -32,6 +32,21 @@ const permissionsData = [ { code: "roles.delete", }, + { + code: "managementAspect.readAll", + }, + { + code: "managementAspect.create", + }, + { + code: "managementAspect.update", + }, + { + code: "managementAspect.delete", + }, + { + code: "managementAspect.restore", + }, ] as const; export type SpecificPermissionCode = (typeof permissionsData)[number]["code"]; diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 252a875..4b863c8 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -3,7 +3,7 @@ import { configDotenv } from "dotenv"; import { Hono } from "hono"; import authRoutes from "./routes/auth/route"; import usersRoute from "./routes/users/route"; -import aspectsRoute from "./routes/managementAspect/route"; +import managementAspectsRoute from "./routes/managementAspect/route"; import { verifyAccessToken } from "./utils/authUtils"; import permissionRoutes from "./routes/permissions/route"; import { cors } from "hono/cors"; @@ -79,7 +79,7 @@ const routes = app .route("/dashboard", dashboardRoutes) .route("/roles", rolesRoute) .route("/dev", devRoutes) - .route("/management-aspect", aspectsRoute) + .route("/management-aspect", managementAspectsRoute) .onError((err, c) => { if (err instanceof DashboardError) { return c.json( diff --git a/apps/backend/src/routes/managementAspect/route.ts b/apps/backend/src/routes/managementAspect/route.ts index 80cf70e..8f0074a 100644 --- a/apps/backend/src/routes/managementAspect/route.ts +++ b/apps/backend/src/routes/managementAspect/route.ts @@ -1,418 +1,495 @@ -import { and, eq, ilike, isNull, or, sql } from "drizzle-orm"; -import { Hono } from "hono"; + import { and, eq, ilike, isNull, or, sql } from "drizzle-orm"; + import { Hono } from "hono"; -import { z } from "zod"; -import { HTTPException } from "hono/http-exception"; -import db from "../../drizzle"; -import { aspects } from "../../drizzle/schema/aspects"; -import { subAspects } from "../../drizzle/schema/subAspects"; -import HonoEnv from "../../types/HonoEnv"; -import requestValidator from "../../utils/requestValidator"; -import authInfo from "../../middlewares/authInfo"; -import checkPermission from "../../middlewares/checkPermission"; + import { z } from "zod"; + import { HTTPException } from "hono/http-exception"; + import db from "../../drizzle"; + import { aspects } from "../../drizzle/schema/aspects"; + import { subAspects } from "../../drizzle/schema/subAspects"; + import HonoEnv from "../../types/HonoEnv"; + import requestValidator from "../../utils/requestValidator"; + import authInfo from "../../middlewares/authInfo"; + import checkPermission from "../../middlewares/checkPermission"; -// Schema for creating and updating aspects -export const aspectFormSchema = z.object({ - name: z.string().min(1).max(50), - subAspects: z - .string() - .refine( - (data) => { - try { - const parsed = JSON.parse(data); - return Array.isArray(parsed); - } catch { - return false; + // Schema for creating and updating aspects + export const aspectFormSchema = z.object({ + name: z.string().min(1).max(50), + subAspects: z + .string() + .refine( + (data) => { + try { + const parsed = JSON.parse(data); + return Array.isArray(parsed); + } catch { + return false; + } + }, + { + message: "Sub Aspects must be an array", } - }, - { - message: "Sub Aspects must be an array", + ) + .optional(), + }); + + export const aspectUpdateSchema = aspectFormSchema.extend({ + subAspects: z.string().optional().or(z.literal("")), + }); + + // Schema for creating and updating subAspects + export const subAspectFormSchema = z.object({ + name: z.string().min(1).max(50), + aspectId: z.string().uuid(), + }); + + export const subAspectUpdateSchema = subAspectFormSchema.extend({}); + + const managementAspectRoute = new Hono() + .use(authInfo) + /** + * Get All Aspects (With Metadata) + * + * Query params: + * - includeTrashed: boolean (default: false) + * - withMetadata: boolean + */ + // Get all aspects + .get( + "/", + checkPermission("managementAspect.readAll"), + requestValidator( + "query", + z.object({ + includeTrashed: z + .string() + .optional() + .transform((v) => v?.toLowerCase() === "true"), + withMetadata: z + .string() + .optional() + .transform((v) => v?.toLowerCase() === "true"), + page: z.coerce.number().int().min(0).default(0), + limit: z.coerce.number().int().min(1).max(1000).default(10), + q: z.string().default(""), + }) + ), + async (c) => { + const { includeTrashed, page, limit, q } = c.req.valid("query"); + + const totalCountQuery = includeTrashed + ? sql`(SELECT count(*) FROM ${aspects})` + : sql`(SELECT count(*) FROM ${aspects} WHERE ${aspects.deletedAt} IS NULL)`; + + const result = await db + .select({ + id: aspects.id, + name: aspects.name, + createdAt: aspects.createdAt, + updatedAt: aspects.updatedAt, + ...(includeTrashed ? { deletedAt: aspects.deletedAt } : {}), + fullCount: totalCountQuery, + }) + .from(aspects) + .where( + and( + includeTrashed ? undefined : isNull(aspects.deletedAt), + q ? or(ilike(aspects.name, q), eq(aspects.id, q)) : undefined + ) + ) + .offset(page * limit) + .limit(limit); + + return c.json({ + data: result.map((d) => ({ ...d, fullCount: undefined })), + _metadata: { + currentPage: page, + totalPages: Math.ceil((Number(result[0]?.fullCount) ?? 0) / limit), + totalItems: Number(result[0]?.fullCount) ?? 0, + perPage: limit, + }, + }); } ) - .optional(), -}); - -export const aspectUpdateSchema = aspectFormSchema.extend({ - subAspects: z.string().optional().or(z.literal("")), -}); - -// Schema for creating and updating subAspects -export const subAspectFormSchema = z.object({ - name: z.string().min(1).max(50), - aspectId: z.string().uuid(), -}); - -export const subAspectUpdateSchema = subAspectFormSchema.extend({}); - -const managementAspectRoute = new Hono() - .use(authInfo) - /** - * Get All Aspects (With Metadata) - * - * Query params: - * - includeTrashed: boolean (default: false) - * - withMetadata: boolean - */ - // Get all aspects - .get( - "/", - requestValidator( - "query", - z.object({ - includeTrashed: z - .string() - .optional() - .transform((v) => v?.toLowerCase() === "true"), - withMetadata: z - .string() - .optional() - .transform((v) => v?.toLowerCase() === "true"), - page: z.coerce.number().int().min(0).default(0), - limit: z.coerce.number().int().min(1).max(1000).default(10), - q: z.string().default(""), - }) - ), - async (c) => { - const { includeTrashed, page, limit, q } = c.req.valid("query"); - - const totalCountQuery = includeTrashed - ? sql`(SELECT count(*) FROM ${aspects})` - : sql`(SELECT count(*) FROM ${aspects} WHERE ${aspects.deletedAt} IS NULL)`; - - const result = await db - .select({ - id: aspects.id, - name: aspects.name, - createdAt: aspects.createdAt, - updatedAt: aspects.updatedAt, - ...(includeTrashed ? { deletedAt: aspects.deletedAt } : {}), - fullCount: totalCountQuery, + + // Get aspect by id + .get( + "/:id", + checkPermission("managementAspect.readAll"), + requestValidator( + "query", + z.object({ + includeTrashed: z.string().default("false"), }) - .from(aspects) - .where( - and( - includeTrashed ? undefined : isNull(aspects.deletedAt), - q ? or(ilike(aspects.name, q), eq(aspects.id, q)) : undefined - ) - ) - .offset(page * limit) - .limit(limit); + ), + async (c) => { + const aspectId = c.req.param("id"); - return c.json({ - data: result.map((d) => ({ ...d, fullCount: undefined })), - _metadata: { - currentPage: page, - totalPages: Math.ceil((Number(result[0]?.fullCount) ?? 0) / limit), - totalItems: Number(result[0]?.fullCount) ?? 0, - perPage: limit, - }, - }); - } - ) - // Get aspect by id - .get( - "/:id", - requestValidator( - "query", - z.object({ - includeTrashed: z.string().default("false"), - }) - ), - async (c) => { - const aspectId = c.req.param("id"); + if (!aspectId) throw new HTTPException(400, { message: "Missing id" }); - if (!aspectId) throw new HTTPException(400, { message: "Missing id" }); + const includeTrashed = + c.req.query("includeTrashed")?.toLowerCase() === "true"; - const includeTrashed = - c.req.query("includeTrashed")?.toLowerCase() === "true"; + const queryResult = await db + .select({ + id: aspects.id, + name: aspects.name, + createdAt: aspects.createdAt, + updatedAt: aspects.updatedAt, + ...(includeTrashed ? { deletedAt: aspects.deletedAt } : {}), + subAspect: { + name: subAspects.name, + id: subAspects.id, + }, + }) + .from(aspects) + .leftJoin(subAspects, eq(aspects.id, subAspects.aspectId)) + .where( + and( + eq(aspects.id, aspectId), + !includeTrashed ? isNull(aspects.deletedAt) : undefined + ) + ); - const queryResult = await db - .select({ - id: aspects.id, - name: aspects.name, - createdAt: aspects.createdAt, - updatedAt: aspects.updatedAt, - ...(includeTrashed ? { deletedAt: aspects.deletedAt } : {}), - subAspect: { - name: subAspects.name, - id: subAspects.id, + if (!queryResult.length) + throw new HTTPException(404, { + message: "The aspect does not exist", + }); + + const subAspectsList = queryResult.reduce((prev, curr) => { + if (!curr.subAspect) return prev; + prev.set(curr.subAspect.id, curr.subAspect.name); + return prev; + }, new Map()); // Map + + const aspectData = { + ...queryResult[0], + subAspect: undefined, + subAspects: Array.from(subAspectsList, ([id, name]) => ({ id, name })), + }; + + return c.json(aspectData); + } + ) + + // Create aspect + .post( + "/", + checkPermission("managementAspect.create"), + requestValidator("json", aspectFormSchema), + async (c) => { + const aspectData = c.req.valid("json"); + + // Validasi untuk mengecek apakah nama aspek sudah ada + const existingAspect = await db + .select() + .from(aspects) + .where(ilike(aspects.name, aspectData.name)); + + if (existingAspect.length > 0) { + throw new HTTPException(400, { message: "Aspect name already existss" }); + } + + const aspect = await db + .insert(aspects) + .values({ + name: aspectData.name, + }) + .returning(); + + if (aspectData.subAspects) { + const subAspectsArray = JSON.parse(aspectData.subAspects) as string[]; + + if (subAspectsArray.length) { + await db.insert(subAspects).values( + subAspectsArray.map((subAspect) => ({ + aspectId: aspect[0].id, + name: subAspect, + })) + ); + } + } + + return c.json( + { + message: "Aspect created successfully", }, - }) - .from(aspects) - .leftJoin(subAspects, eq(aspects.id, subAspects.aspectId)) - .where( - and( - eq(aspects.id, aspectId), - !includeTrashed ? isNull(aspects.deletedAt) : undefined - ) + 201 ); - - if (!queryResult.length) - throw new HTTPException(404, { - message: "The aspect does not exist", - }); - - const subAspectsList = queryResult.reduce((prev, curr) => { - if (!curr.subAspect) return prev; - prev.set(curr.subAspect.id, curr.subAspect.name); - return prev; - }, new Map()); // Map - - const aspectData = { - ...queryResult[0], - subAspect: undefined, - subAspects: Array.from(subAspectsList, ([id, name]) => ({ id, name })), - }; - - return c.json(aspectData); - } - ) - // Create aspect - .post( - "/", - requestValidator("json", aspectFormSchema), - async (c) => { - const aspectData = c.req.valid("json"); - - const aspect = await db - .insert(aspects) - .values({ - name: aspectData.name, - }) - .returning(); - - if (aspectData.subAspects) { - const subAspectsArray = JSON.parse(aspectData.subAspects) as string[]; - - if (subAspectsArray.length) { - await db.insert(subAspects).values( - subAspectsArray.map((subAspect) => ({ - aspectId: aspect[0].id, - name: subAspect, - })) - ); - } } + ) - return c.json( - { - message: "Aspect created successfully", - }, - 201 - ); - } - ) - // Update aspect - .patch( - "/:id", - requestValidator("json", aspectUpdateSchema), - async (c) => { - const aspectId = c.req.param("id"); - const aspectData = c.req.valid("json"); + // Update aspect + .patch( + "/:id", + checkPermission("managementAspect.update"), + requestValidator("json", aspectUpdateSchema), + async (c) => { + const aspectId = c.req.param("id"); + const aspectData = c.req.valid("json"); - const aspect = await db - .select() - .from(aspects) - .where(and(eq(aspects.id, aspectId), isNull(aspects.deletedAt))); - - if (!aspect[0]) return c.notFound(); - - await db - .update(aspects) - .set({ - ...aspectData, - updatedAt: new Date(), - }) - .where(eq(aspects.id, aspectId)); - - if (aspectData.subAspects) { - const subAspectsArray = JSON.parse(aspectData.subAspects) as string[]; - - await db.delete(subAspects).where(eq(subAspects.aspectId, aspectId)); - - if (subAspectsArray.length) { - await db.insert(subAspects).values( - subAspectsArray.map((subAspect) => ({ - aspectId: aspectId, - name: subAspect, - })) + // Validasi untuk mengecek apakah nama aspek baru sudah ada + const existingAspect = await db + .select() + .from(aspects) + .where( + and( + ilike(aspects.name, aspectData.name), + isNull(aspects.deletedAt), + sql`${aspects.id} <> ${aspectId}` + ) ); + + if (existingAspect.length > 0) { + throw new HTTPException(400, { message: "Aspect name already exists" }); } - } - return c.json({ - message: "Aspect updated successfully", - }); - } - ) - // Delete aspect - .delete( - "/:id", - requestValidator( - "form", - z.object({ - skipTrash: z.string().default("false"), - }) - ), - async (c) => { - const aspectId = c.req.param("id"); + const aspect = await db + .select() + .from(aspects) + .where(and(eq(aspects.id, aspectId), isNull(aspects.deletedAt))); - const skipTrash = c.req.valid("form").skipTrash.toLowerCase() === "true"; + if (!aspect[0]) return c.notFound(); - const aspect = await db - .select() - .from(aspects) - .where(and(eq(aspects.id, aspectId), skipTrash ? undefined : isNull(aspects.deletedAt))); - - if (!aspect[0]) - throw new HTTPException(404, { - message: "The aspect is not found", - }); - - if (skipTrash) { - await db.delete(aspects).where(eq(aspects.id, aspectId)); - } else { await db .update(aspects) .set({ - deletedAt: new Date(), + ...aspectData, + updatedAt: new Date(), }) - .where(and(eq(aspects.id, aspectId), isNull(aspects.deletedAt))); - } - return c.json({ - message: "Aspect deleted successfully", - }); - } - ) - // Undo delete - .patch( - "/restore/:id", - async (c) => { - const aspectId = c.req.param("id"); + .where(eq(aspects.id, aspectId)); - const aspect = (await db.select().from(aspects).where(eq(aspects.id, aspectId)))[0]; + if (aspectData.subAspects) { + const subAspectsArray = JSON.parse(aspectData.subAspects) as string[]; - if (!aspect) return c.notFound(); + await db.delete(subAspects).where(eq(subAspects.aspectId, aspectId)); - if (!aspect.deletedAt) { - throw new HTTPException(400, { - message: "The aspect is not deleted", + if (subAspectsArray.length) { + await db.insert(subAspects).values( + subAspectsArray.map((subAspect) => ({ + aspectId: aspectId, + name: subAspect, + })) + ); + } + } + + return c.json({ + message: "Aspect updated successfully", }); } + ) - await db.update(aspects).set({ deletedAt: null }).where(eq(aspects.id, aspectId)); - - return c.json({ - message: "Aspect restored successfully", - }); - } - ) - // Create sub aspect - .post( - "/subAspect", - requestValidator("json", subAspectFormSchema), - async (c) => { - const subAspectData = c.req.valid("json"); - - const aspect = await db - .select() - .from(aspects) - .where(and(eq(aspects.id, subAspectData.aspectId), isNull(aspects.deletedAt))); - - if (!aspect[0]) - throw new HTTPException(404, { - message: "The aspect is not found", - }); - - await db.insert(subAspects).values(subAspectData); - - return c.json( - { - message: "Sub aspect created successfully", - }, - 201 - ); - } - ) - // Update sub aspect - .patch( - "/subAspect/:id", - requestValidator("json", subAspectUpdateSchema), - async (c) => { - const subAspectId = c.req.param("id"); - const subAspectData = c.req.valid("json"); - - const subAspect = await db - .select() - .from(subAspects) - .where(eq(subAspects.id, subAspectId)); - - if (!subAspect[0]) - throw new HTTPException(404, { - message: "The sub aspect is not found", - }); - - await db - .update(subAspects) - .set({ - ...subAspectData, - updatedAt: new Date(), + // Delete aspect + .delete( + "/:id", + checkPermission("managementAspect.delete"), + requestValidator( + "form", + z.object({ + skipTrash: z.string().default("false"), }) - .where(eq(subAspects.id, subAspectId)); + ), + async (c) => { + const aspectId = c.req.param("id"); - return c.json({ - message: "Sub aspect updated successfully", - }); - } - ) - // Delete sub aspect - .delete( - "/subAspect/:id", - async (c) => { - const subAspectId = c.req.param("id"); + const skipTrash = c.req.valid("form").skipTrash.toLowerCase() === "true"; - const subAspect = await db - .select() - .from(subAspects) - .where(eq(subAspects.id, subAspectId)); + const aspect = await db + .select() + .from(aspects) + .where(and(eq(aspects.id, aspectId), skipTrash ? undefined : isNull(aspects.deletedAt))); - if (!subAspect[0]) - throw new HTTPException(404, { - message: "The sub aspect is not found", + if (!aspect[0]) + throw new HTTPException(404, { + message: "The aspect is not found", + }); + + if (skipTrash) { + await db.delete(aspects).where(eq(aspects.id, aspectId)); + } else { + await db + .update(aspects) + .set({ + deletedAt: new Date(), + }) + .where(and(eq(aspects.id, aspectId), isNull(aspects.deletedAt))); + } + return c.json({ + message: "Aspect deleted successfully", }); + } + ) - await db.delete(subAspects).where(eq(subAspects.id, subAspectId)); + // Undo delete + .patch( + "/restore/:id", + checkPermission("managementAspect.restore"), + async (c) => { + const aspectId = c.req.param("id"); - return c.json({ - message: "Sub aspect deleted successfully", - }); - } - ) - // Get sub aspects by aspect ID - .get( - "/subAspects/:aspectId", - async (c) => { - const aspectId = c.req.param("aspectId"); + const aspect = (await db.select().from(aspects).where(eq(aspects.id, aspectId)))[0]; - const aspect = await db - .select() - .from(aspects) - .where(and(eq(aspects.id, aspectId), isNull(aspects.deletedAt))); + if (!aspect) return c.notFound(); - if (!aspect[0]) - throw new HTTPException(404, { - message: "The aspect is not found", + if (!aspect.deletedAt) { + throw new HTTPException(400, { + message: "The aspect is not deleted", + }); + } + + await db.update(aspects).set({ deletedAt: null }).where(eq(aspects.id, aspectId)); + + return c.json({ + message: "Aspect restored successfully", }); + } + ) - const subAspectsData = await db - .select() - .from(subAspects) - .where(eq(subAspects.aspectId, aspectId)); + // Get sub aspects by aspect ID + .get( + "/subAspects/:aspectId", + checkPermission("managementAspect.readAll"), + async (c) => { + const aspectId = c.req.param("aspectId"); - return c.json({ - subAspects: subAspectsData, - }); - } - ); + const aspect = await db + .select() + .from(aspects) + .where(and(eq(aspects.id, aspectId), isNull(aspects.deletedAt))); + + if (!aspect[0]) + throw new HTTPException(404, { + message: "The aspect is not found", + }); + + const subAspectsData = await db + .select() + .from(subAspects) + .where(eq(subAspects.aspectId, aspectId)); + + return c.json({ + subAspects: subAspectsData, + }); + } + ) + + // Create sub aspect + .post( + "/subAspect", + checkPermission("managementAspect.create"), + requestValidator("json", subAspectFormSchema), + async (c) => { + const subAspectData = c.req.valid("json"); + + // Validasi untuk mengecek apakah nama sub aspek sudah ada + const existingSubAspect = await db + .select() + .from(subAspects) + .where( + and( + ilike(subAspects.name, subAspectData.name), + eq(subAspects.aspectId, subAspectData.aspectId) + ) + ); + + if (existingSubAspect.length > 0) { + throw new HTTPException(400, { message: "Nama Sub Aspek sudah tersedia!" }); + } + + const aspect = await db + .select() + .from(aspects) + .where(and(eq(aspects.id, subAspectData.aspectId), isNull(aspects.deletedAt))); + + if (!aspect[0]) + throw new HTTPException(404, { + message: "The aspect is not found", + }); + + await db.insert(subAspects).values(subAspectData); + + return c.json( + { + message: "Sub aspect created successfully", + }, + 201 + ); + } + ) + + // Update sub aspect + .patch( + "/subAspect/:id", + checkPermission("managementAspect.update"), + requestValidator("json", subAspectUpdateSchema), + async (c) => { + const subAspectId = c.req.param("id"); + const subAspectData = c.req.valid("json"); + + // Validasi untuk mengecek apakah nama sub aspek baru sudah ada + const existingSubAspect = await db + .select() + .from(subAspects) + .where( + and( + ilike(subAspects.name, subAspectData.name), + eq(subAspects.aspectId, subAspectData.aspectId), + sql`${subAspects.id} <> ${subAspectId}` + ) + ); + + if (existingSubAspect.length > 0) { + throw new HTTPException(400, { message: "Name Sub Aspect already exists" }); + } + + const subAspect = await db + .select() + .from(subAspects) + .where(eq(subAspects.id, subAspectId)); + + if (!subAspect[0]) + throw new HTTPException(404, { + message: "The sub aspect is not found", + }); + + await db + .update(subAspects) + .set({ + ...subAspectData, + updatedAt: new Date(), + }) + .where(eq(subAspects.id, subAspectId)); + + return c.json({ + message: "Sub aspect updated successfully", + }); + } + ) + + // Delete sub aspect + .delete( + "/subAspect/:id", + checkPermission("managementAspect.delete"), + async (c) => { + const subAspectId = c.req.param("id"); + + const subAspect = await db + .select() + .from(subAspects) + .where(eq(subAspects.id, subAspectId)); + + if (!subAspect[0]) + throw new HTTPException(404, { + message: "The sub aspect is not found", + }); + + await db.delete(subAspects).where(eq(subAspects.id, subAspectId)); + + return c.json({ + message: "Sub aspect deleted successfully", + }); + } + ); + + export default managementAspectRoute; -export default managementAspectRoute; From 02961782e7ba8e03ac80cf6b70b6f047b4c0de7a Mon Sep 17 00:00:00 2001 From: abiyasa05 Date: Wed, 14 Aug 2024 10:36:14 +0700 Subject: [PATCH 06/12] fix: revise register --- apps/backend/src/routes/register/route.ts | 235 ++++++++++------------ 1 file changed, 105 insertions(+), 130 deletions(-) diff --git a/apps/backend/src/routes/register/route.ts b/apps/backend/src/routes/register/route.ts index a8fae90..ef6fd2a 100644 --- a/apps/backend/src/routes/register/route.ts +++ b/apps/backend/src/routes/register/route.ts @@ -3,19 +3,19 @@ import { HTTPException } from "hono/http-exception"; import db from "../../drizzle"; import { respondents } from "../../drizzle/schema/respondents"; import { users } from "../../drizzle/schema/users"; +import { rolesSchema } from "../../drizzle/schema/roles"; import { rolesToUsers } from "../../drizzle/schema/rolesToUsers"; import { hashPassword } from "../../utils/passwordUtils"; import requestValidator from "../../utils/requestValidator"; import authInfo from "../../middlewares/authInfo"; -import checkPermission from "../../middlewares/checkPermission"; -import { and, eq, or } from "drizzle-orm"; +import { or, eq } from "drizzle-orm"; import { z } from "zod"; import HonoEnv from "../../types/HonoEnv"; const registerFormSchema = z.object({ name: z.string().min(1).max(255), username: z.string().min(1).max(255), - email: z.string().email().optional(), + email: z.string().email(), password: z.string().min(6), companyName: z.string().min(1).max(255), position: z.string().min(1).max(255), @@ -23,135 +23,110 @@ const registerFormSchema = z.object({ address: z.string().min(1), phoneNumber: z.string().min(1).max(13), isEnabled: z.string().default("false"), - roles: z - .string() - .refine( - (data) => { - try { - const parsed = JSON.parse(data); - return Array.isArray(parsed); - } catch { - return false; - } - }, - { - message: "Roles must be an array", - } - ) - .optional(), }); const respondentsRoute = new Hono() .use(authInfo) - .post( - "/", - checkPermission("register.create"), - requestValidator("json", registerFormSchema), - async (c) => { - const formData = c.req.valid("json"); - - console.log("Form Data:", formData); - - // Build conditions based on available formData - const conditions = []; - if (formData.email) { - conditions.push(eq(users.email, formData.email)); - } - conditions.push(eq(users.username, formData.username)); - - const existingUser = await db - .select() - .from(users) - .where(or(...conditions)); - - console.log("Existing Users:", existingUser); - - const existingRespondent = await db - .select() - .from(respondents) - .where(eq(respondents.phoneNumber, formData.phoneNumber)); - - console.log("Existing Respondents:", existingRespondent); - - if (existingUser.length > 0) { - throw new HTTPException(400, { - message: "Email atau username sudah terdaftar", - }); - } - - if (existingRespondent.length > 0) { - throw new HTTPException(400, { - message: "Nomor HP sudah terdaftar", - }); - } - - // Hash the password - const hashedPassword = await hashPassword(formData.password); - - console.log("Hashed Password:", hashedPassword); - - // Start a transaction - try { - const result = await db.transaction(async (trx) => { - // Create user - const [newUser] = await trx - .insert(users) - .values({ - name: formData.name, - username: formData.username, - email: formData.email, - password: hashedPassword, - isEnabled: formData.isEnabled?.toLowerCase() === "true" || true, - }) - .returning(); - - console.log("New User:", newUser); - - // Create respondent - await trx.insert(respondents).values({ - companyName: formData.companyName, - position: formData.position, - workExperience: formData.workExperience, - address: formData.address, - phoneNumber: formData.phoneNumber, - userId: newUser.id, - }); - - console.log("Respondent Created for User ID:", newUser.id); - - // If roles are included in the request, add to rolesToUsers - if (formData.roles) { - try { - const roles = JSON.parse(formData.roles) as string[]; - if (roles.length) { - await trx.insert(rolesToUsers).values( - roles.map((role) => ({ - userId: newUser.id, - roleId: role, - })) - ); - } - } catch (error) { - console.error("Error parsing roles:", error); - throw new HTTPException(400, { - message: "Invalid roles format", - }); - } - } - - return newUser; - }); - - return c.json({ - message: "User and respondent created successfully", - }, 201); - } catch (error) { - console.error("Error creating user and respondent:", error); - throw new HTTPException(500, { - message: "Error creating user and respondent", - }); - } - } - ); + //post user + .post("/", requestValidator("json", registerFormSchema), async (c) => { + const formData = c.req.valid("json"); -export default respondentsRoute; \ No newline at end of file + // Check if the provided email or username is already exists in database + const conditions = []; + if (formData.email) { + conditions.push(eq(users.email, formData.email)); + } + conditions.push(eq(users.username, formData.username)); + + const existingUser = await db + .select() + .from(users) + .where( + or( + eq(users.email, formData.email), + eq(users.username, formData.username) + ) + ); + + const existingRespondent = await db + .select() + .from(respondents) + .where(eq(respondents.phoneNumber, formData.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(formData.password); + + // Start a transaction + const result = await db.transaction(async (trx) => { + // Create user + const [newUser] = await trx + .insert(users) + .values({ + name: formData.name, + username: formData.username, + email: formData.email, + password: hashedPassword, + isEnabled: formData.isEnabled?.toLowerCase() === "true" || true, + }) + .returning() + .catch(() => { + throw new HTTPException(500, { message: "Error creating user" }); + }); + + // Create respondent + await trx + .insert(respondents) + .values({ + companyName: formData.companyName, + position: formData.position, + workExperience: formData.workExperience, + address: formData.address, + phoneNumber: formData.phoneNumber, + userId: newUser.id, + }) + .catch(() => { + throw new HTTPException(500, { + message: "Error creating respondent", + }); + }); + + // 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 new HTTPException(500, { message: "Role 'user' not found" }); + } + + await trx.insert(rolesToUsers).values({ + userId: newUser.id, + roleId: role.id, + }); + + return newUser; + }); + + return c.json( + { + message: "User created successfully", + }, + 201 + ); + }); + +export default respondentsRoute; From 38f153fe765b3c8ad1c2dfa5163501a43136e344 Mon Sep 17 00:00:00 2001 From: abiyasa05 Date: Wed, 14 Aug 2024 10:37:35 +0700 Subject: [PATCH 07/12] fx: delete register permission --- apps/backend/src/data/permissions.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/backend/src/data/permissions.ts b/apps/backend/src/data/permissions.ts index 8fc61fe..1e91356 100644 --- a/apps/backend/src/data/permissions.ts +++ b/apps/backend/src/data/permissions.ts @@ -32,9 +32,6 @@ const permissionsData = [ { code: "roles.delete", }, - { - code: "register.create", - }, ] as const; export type SpecificPermissionCode = (typeof permissionsData)[number]["code"]; From c6e6ca1efe1f610cef7fc46a84f05562510957f1 Mon Sep 17 00:00:00 2001 From: abiyasa05 Date: Wed, 14 Aug 2024 10:38:51 +0700 Subject: [PATCH 08/12] update: add user roles --- apps/backend/src/data/roles.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/data/roles.ts b/apps/backend/src/data/roles.ts index bc087b5..d1c46bb 100644 --- a/apps/backend/src/data/roles.ts +++ b/apps/backend/src/data/roles.ts @@ -20,10 +20,10 @@ const roleData: RoleData[] = [ { code: "user", description: - "Has full access to the system and can manage all features and settings", + "User with standard access rights for general usage of the application.", isActive: true, name: "User", - permissions: ["register.create"], + permissions: permissionsData.map((permission) => permission.code), }, ]; From 625420417e295de6d5485dafdffc43f6f3ed3385 Mon Sep 17 00:00:00 2001 From: percyfikri Date: Wed, 14 Aug 2024 14:22:58 +0700 Subject: [PATCH 09/12] update : Revision after first pull request on management-aspect --- .../src/routes/managementAspect/route.ts | 363 +++++++++--------- 1 file changed, 180 insertions(+), 183 deletions(-) diff --git a/apps/backend/src/routes/managementAspect/route.ts b/apps/backend/src/routes/managementAspect/route.ts index 8f0074a..dbbf1fb 100644 --- a/apps/backend/src/routes/managementAspect/route.ts +++ b/apps/backend/src/routes/managementAspect/route.ts @@ -1,51 +1,52 @@ - import { and, eq, ilike, isNull, or, sql } from "drizzle-orm"; - import { Hono } from "hono"; +import { and, eq, ilike, isNull, or, sql } from "drizzle-orm"; +import { Hono } from "hono"; - import { z } from "zod"; - import { HTTPException } from "hono/http-exception"; - import db from "../../drizzle"; - import { aspects } from "../../drizzle/schema/aspects"; - import { subAspects } from "../../drizzle/schema/subAspects"; - import HonoEnv from "../../types/HonoEnv"; - import requestValidator from "../../utils/requestValidator"; - import authInfo from "../../middlewares/authInfo"; - import checkPermission from "../../middlewares/checkPermission"; +import { z } from "zod"; +import db from "../../drizzle"; +import { aspects } from "../../drizzle/schema/aspects"; +import { subAspects } from "../../drizzle/schema/subAspects"; +import HonoEnv from "../../types/HonoEnv"; +import requestValidator from "../../utils/requestValidator"; +import authInfo from "../../middlewares/authInfo"; +import checkPermission from "../../middlewares/checkPermission"; +import { forbidden } from "../../errors/DashboardError"; +import { notFound } from "../../errors/DashboardError"; - // Schema for creating and updating aspects - export const aspectFormSchema = z.object({ - name: z.string().min(1).max(50), - subAspects: z - .string() - .refine( - (data) => { - try { - const parsed = JSON.parse(data); - return Array.isArray(parsed); - } catch { - return false; - } - }, - { - message: "Sub Aspects must be an array", +// Schema for creating and updating aspects +export const aspectFormSchema = z.object({ + name: z.string().min(1).max(50), + subAspects: z + .string() + .refine( + (data) => { + try { + const parsed = JSON.parse(data); + return Array.isArray(parsed); + } catch { + return false; } - ) - .optional(), - }); + }, + { + message: "Sub Aspects must be an array", + } + ) + .optional(), +}); - export const aspectUpdateSchema = aspectFormSchema.extend({ - subAspects: z.string().optional().or(z.literal("")), - }); +export const aspectUpdateSchema = aspectFormSchema.extend({ + subAspects: z.string().optional().or(z.literal("")), +}); - // Schema for creating and updating subAspects - export const subAspectFormSchema = z.object({ - name: z.string().min(1).max(50), - aspectId: z.string().uuid(), - }); +// Schema for creating and updating subAspects +export const subAspectFormSchema = z.object({ + name: z.string().min(1).max(50), + aspectId: z.string().uuid(), +}); - export const subAspectUpdateSchema = subAspectFormSchema.extend({}); +export const subAspectUpdateSchema = subAspectFormSchema.extend({}); - const managementAspectRoute = new Hono() - .use(authInfo) +const managementAspectRoute = new Hono() + .use(authInfo) /** * Get All Aspects (With Metadata) * @@ -53,6 +54,7 @@ * - includeTrashed: boolean (default: false) * - withMetadata: boolean */ + // Get all aspects .get( "/", @@ -76,8 +78,8 @@ async (c) => { const { includeTrashed, page, limit, q } = c.req.valid("query"); - const totalCountQuery = includeTrashed - ? sql`(SELECT count(*) FROM ${aspects})` + const totalCountQuery = includeTrashed + ? sql`(SELECT count(*) FROM ${aspects})` : sql`(SELECT count(*) FROM ${aspects} WHERE ${aspects.deletedAt} IS NULL)`; const result = await db @@ -110,7 +112,7 @@ }); } ) - + // Get aspect by id .get( "/:id", @@ -124,10 +126,12 @@ async (c) => { const aspectId = c.req.param("id"); - if (!aspectId) throw new HTTPException(400, { message: "Missing id" }); + if (!aspectId) + throw notFound({ + message: "Missing id", + }); - const includeTrashed = - c.req.query("includeTrashed")?.toLowerCase() === "true"; + const includeTrashed = c.req.query("includeTrashed")?.toLowerCase() === "true"; const queryResult = await db .select({ @@ -143,15 +147,10 @@ }) .from(aspects) .leftJoin(subAspects, eq(aspects.id, subAspects.aspectId)) - .where( - and( - eq(aspects.id, aspectId), - !includeTrashed ? isNull(aspects.deletedAt) : undefined - ) - ); + .where(and(eq(aspects.id, aspectId), !includeTrashed ? isNull(aspects.deletedAt) : undefined)); if (!queryResult.length) - throw new HTTPException(404, { + throw forbidden({ message: "The aspect does not exist", }); @@ -172,42 +171,44 @@ ) // Create aspect - .post( - "/", - checkPermission("managementAspect.create"), - requestValidator("json", aspectFormSchema), + .post("/", + checkPermission("managementAspect.create"), + requestValidator("json", aspectFormSchema), async (c) => { const aspectData = c.req.valid("json"); - // Validasi untuk mengecek apakah nama aspek sudah ada - const existingAspect = await db - .select() - .from(aspects) - .where(ilike(aspects.name, aspectData.name)); + // Validation to check if the aspect name already exists + const existingAspect = await db + .select() + .from(aspects) + .where(eq(aspects.name, aspectData.name)); - if (existingAspect.length > 0) { - throw new HTTPException(400, { message: "Aspect name already existss" }); - } - - const aspect = await db - .insert(aspects) - .values({ - name: aspectData.name, - }) - .returning(); - - if (aspectData.subAspects) { - const subAspectsArray = JSON.parse(aspectData.subAspects) as string[]; - - if (subAspectsArray.length) { - await db.insert(subAspects).values( - subAspectsArray.map((subAspect) => ({ - aspectId: aspect[0].id, - name: subAspect, - })) - ); - } + if (existingAspect.length > 0) { + throw forbidden({ + message: "Aspect name already exists", + }); + } + + const aspect = await db + .insert(aspects) + .values({ + name: aspectData.name, + }) + .returning(); + + // if sub-aspects are available, parse them into a string array + if (aspectData.subAspects) { + const subAspectsArray = JSON.parse(aspectData.subAspects) as string[]; + // if there are sub-aspects, insert them into the database + if (subAspectsArray.length) { + await db.insert(subAspects).values( + subAspectsArray.map((subAspect) => ({ + aspectId: aspect[0].id, + name: subAspect, + })) + ); } + } return c.json( { @@ -220,58 +221,61 @@ // Update aspect .patch( - "/:id", - checkPermission("managementAspect.update"), - requestValidator("json", aspectUpdateSchema), + "/:id", + checkPermission("managementAspect.update"), + requestValidator("json", aspectUpdateSchema), async (c) => { const aspectId = c.req.param("id"); const aspectData = c.req.valid("json"); - // Validasi untuk mengecek apakah nama aspek baru sudah ada - const existingAspect = await db - .select() - .from(aspects) - .where( - and( - ilike(aspects.name, aspectData.name), - isNull(aspects.deletedAt), - sql`${aspects.id} <> ${aspectId}` - ) - ); + // Validation to check if the new aspect name already exists + const existingAspect = await db + .select() + .from(aspects) + .where( + and( + eq(aspects.name, aspectData.name), + isNull(aspects.deletedAt), + sql`${aspects.id} <> ${aspectId}` + ) + ); - if (existingAspect.length > 0) { - throw new HTTPException(400, { message: "Aspect name already exists" }); - } + if (existingAspect.length > 0) { + throw forbidden({ + message: "Aspect name already exists", + }); + } - const aspect = await db - .select() - .from(aspects) - .where(and(eq(aspects.id, aspectId), isNull(aspects.deletedAt))); + const aspect = await db + .select() + .from(aspects) + .where(and(eq(aspects.id, aspectId), isNull(aspects.deletedAt))); - if (!aspect[0]) return c.notFound(); + if (!aspect[0]) throw notFound(); - await db - .update(aspects) - .set({ - ...aspectData, - updatedAt: new Date(), - }) - .where(eq(aspects.id, aspectId)); + await db + .update(aspects) + .set({ + ...aspectData, + updatedAt: new Date(), + }) + .where(eq(aspects.id, aspectId)); - if (aspectData.subAspects) { - const subAspectsArray = JSON.parse(aspectData.subAspects) as string[]; + //Update for Sub-Aspects + // if (aspectData.subAspects) { + // const subAspectsArray = JSON.parse(aspectData.subAspects) as string[]; - await db.delete(subAspects).where(eq(subAspects.aspectId, aspectId)); + // await db.delete(subAspects).where(eq(subAspects.aspectId, aspectId)); - if (subAspectsArray.length) { - await db.insert(subAspects).values( - subAspectsArray.map((subAspect) => ({ - aspectId: aspectId, - name: subAspect, - })) - ); - } - } + // if (subAspectsArray.length) { + // await db.insert(subAspects).values( + // subAspectsArray.map((subAspect) => ({ + // aspectId: aspectId, + // name: subAspect, + // })) + // ); + // } + // } return c.json({ message: "Aspect updated successfully", @@ -286,34 +290,31 @@ requestValidator( "form", z.object({ - skipTrash: z.string().default("false"), + // skipTrash: z.string().default("false"), }) ), async (c) => { const aspectId = c.req.param("id"); - const skipTrash = c.req.valid("form").skipTrash.toLowerCase() === "true"; + // const skipTrash = c.req.valid("form").skipTrash.toLowerCase() === "true"; const aspect = await db .select() .from(aspects) - .where(and(eq(aspects.id, aspectId), skipTrash ? undefined : isNull(aspects.deletedAt))); + .where(and(eq(aspects.id, aspectId), isNull(aspects.deletedAt))); if (!aspect[0]) - throw new HTTPException(404, { + throw notFound({ message: "The aspect is not found", }); - if (skipTrash) { - await db.delete(aspects).where(eq(aspects.id, aspectId)); - } else { - await db + await db .update(aspects) .set({ deletedAt: new Date(), }) - .where(and(eq(aspects.id, aspectId), isNull(aspects.deletedAt))); - } + .where(eq(aspects.id, aspectId)); + return c.json({ message: "Aspect deleted successfully", }); @@ -322,8 +323,8 @@ // Undo delete .patch( - "/restore/:id", - checkPermission("managementAspect.restore"), + "/restore/:id", + checkPermission("managementAspect.restore"), async (c) => { const aspectId = c.req.param("id"); @@ -332,7 +333,7 @@ if (!aspect) return c.notFound(); if (!aspect.deletedAt) { - throw new HTTPException(400, { + throw forbidden({ message: "The aspect is not deleted", }); } @@ -345,20 +346,22 @@ } ) - // Get sub aspects by aspect ID + // Get sub aspects by aspect ID .get( - "/subAspects/:aspectId", - checkPermission("managementAspect.readAll"), + "/subAspects/:aspectId", + checkPermission("managementAspect.readAll"), async (c) => { const aspectId = c.req.param("aspectId"); const aspect = await db .select() .from(aspects) - .where(and(eq(aspects.id, aspectId), isNull(aspects.deletedAt))); + .where(and( + eq(aspects.id, aspectId), + isNull(aspects.deletedAt))); if (!aspect[0]) - throw new HTTPException(404, { + throw notFound({ message: "The aspect is not found", }); @@ -375,34 +378,35 @@ // Create sub aspect .post( - "/subAspect", - checkPermission("managementAspect.create"), - requestValidator("json", subAspectFormSchema), + "/subAspect", + checkPermission("managementAspect.create"), + requestValidator("json", subAspectFormSchema), async (c) => { const subAspectData = c.req.valid("json"); - // Validasi untuk mengecek apakah nama sub aspek sudah ada + // Validation to check if the sub aspect name already exists const existingSubAspect = await db .select() .from(subAspects) .where( and( - ilike(subAspects.name, subAspectData.name), - eq(subAspects.aspectId, subAspectData.aspectId) - ) - ); + eq(subAspects.name, subAspectData.name), + eq(subAspects.aspectId, subAspectData.aspectId))); if (existingSubAspect.length > 0) { - throw new HTTPException(400, { message: "Nama Sub Aspek sudah tersedia!" }); + throw forbidden({ message: "Nama Sub Aspek sudah tersedia!" }); } - const aspect = await db + const [aspect] = await db .select() .from(aspects) - .where(and(eq(aspects.id, subAspectData.aspectId), isNull(aspects.deletedAt))); + .where( + and( + eq(aspects.id, subAspectData.aspectId), + isNull(aspects.deletedAt))); - if (!aspect[0]) - throw new HTTPException(404, { + if (!aspect) + throw forbidden({ message: "The aspect is not found", }); @@ -419,36 +423,25 @@ // Update sub aspect .patch( - "/subAspect/:id", - checkPermission("managementAspect.update"), - requestValidator("json", subAspectUpdateSchema), + "/subAspect/:id", checkPermission("managementAspect.update"), + requestValidator("json", subAspectUpdateSchema), async (c) => { const subAspectId = c.req.param("id"); const subAspectData = c.req.valid("json"); - // Validasi untuk mengecek apakah nama sub aspek baru sudah ada + // Validation to check if the new sub aspect name already exists const existingSubAspect = await db .select() .from(subAspects) .where( - and( - ilike(subAspects.name, subAspectData.name), - eq(subAspects.aspectId, subAspectData.aspectId), - sql`${subAspects.id} <> ${subAspectId}` - ) - ); + eq(subAspects.aspectId, subAspectData.aspectId)); if (existingSubAspect.length > 0) { - throw new HTTPException(400, { message: "Name Sub Aspect already exists" }); + throw forbidden({ message: "Name Sub Aspect already exists" }); } - const subAspect = await db - .select() - .from(subAspects) - .where(eq(subAspects.id, subAspectId)); - - if (!subAspect[0]) - throw new HTTPException(404, { + if (!existingSubAspect[0]) + throw notFound({ message: "The sub aspect is not found", }); @@ -463,13 +456,12 @@ return c.json({ message: "Sub aspect updated successfully", }); - } - ) + }) // Delete sub aspect .delete( - "/subAspect/:id", - checkPermission("managementAspect.delete"), + "/subAspect/:id", + checkPermission("managementAspect.delete"), async (c) => { const subAspectId = c.req.param("id"); @@ -479,11 +471,17 @@ .where(eq(subAspects.id, subAspectId)); if (!subAspect[0]) - throw new HTTPException(404, { + throw notFound({ message: "The sub aspect is not found", }); - await db.delete(subAspects).where(eq(subAspects.id, subAspectId)); + await db + .update(subAspects) + .set({ + deletedAt: new Date(), + }) + .where(eq(subAspects.id, subAspectId)); + // await db.delete(subAspects).where(eq(subAspects.id, subAspectId)); return c.json({ message: "Sub aspect deleted successfully", @@ -491,5 +489,4 @@ } ); - export default managementAspectRoute; - +export default managementAspectRoute; From 7daee35feedb24c12d52253ca4b7950958f32d48 Mon Sep 17 00:00:00 2001 From: abiyasa05 Date: Sun, 18 Aug 2024 19:06:22 +0700 Subject: [PATCH 10/12] fix: change to DashboardError --- apps/backend/src/routes/register/route.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/routes/register/route.ts b/apps/backend/src/routes/register/route.ts index ef6fd2a..4be7398 100644 --- a/apps/backend/src/routes/register/route.ts +++ b/apps/backend/src/routes/register/route.ts @@ -11,6 +11,7 @@ import authInfo from "../../middlewares/authInfo"; import { or, eq } from "drizzle-orm"; import { z } from "zod"; import HonoEnv from "../../types/HonoEnv"; +import { notFound } from "../../errors/DashboardError"; const registerFormSchema = z.object({ name: z.string().min(1).max(255), @@ -109,9 +110,7 @@ const respondentsRoute = new Hono() .where(eq(rolesSchema.code, "user")) .limit(1); - if (!role) { - throw new HTTPException(500, { message: "Role 'user' not found" }); - } + if (!role) throw notFound(); await trx.insert(rolesToUsers).values({ userId: newUser.id, From 001327cc97623377b5d51ae764665b188eda71b6 Mon Sep 17 00:00:00 2001 From: percyfikri Date: Sun, 18 Aug 2024 22:36:20 +0700 Subject: [PATCH 11/12] update : Second revision on management-aspect --- apps/backend/src/routes/managementAspect/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/routes/managementAspect/route.ts b/apps/backend/src/routes/managementAspect/route.ts index dbbf1fb..da8a140 100644 --- a/apps/backend/src/routes/managementAspect/route.ts +++ b/apps/backend/src/routes/managementAspect/route.ts @@ -330,7 +330,7 @@ const managementAspectRoute = new Hono() const aspect = (await db.select().from(aspects).where(eq(aspects.id, aspectId)))[0]; - if (!aspect) return c.notFound(); + if (!aspect) throw notFound(); if (!aspect.deletedAt) { throw forbidden({ From 19133562edb833c01131f311d749aa72a1ddcfda Mon Sep 17 00:00:00 2001 From: abiyasa05 Date: Mon, 19 Aug 2024 11:50:37 +0700 Subject: [PATCH 12/12] update: add route for register --- apps/backend/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 3c714d3..704b19a 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -81,6 +81,7 @@ const routes = app .route("/roles", rolesRoute) .route("/dev", devRoutes) .route("/questions", questionsRoute) + .route("/register", respondentsRoute) .onError((err, c) => { if (err instanceof DashboardError) { return c.json(