From 951115fcce03eceea05fd3c27000ab686e941480 Mon Sep 17 00:00:00 2001 From: sianida26 Date: Wed, 8 May 2024 11:29:12 +0700 Subject: [PATCH] Added auth and permission check middleware --- apps/backend/src/middlewares/authInfo.ts | 74 ++++++++++ .../backend/src/middlewares/authMiddleware.ts | 0 .../src/middlewares/checkPermission.ts | 21 +++ apps/backend/src/routes/dev/route.ts | 18 ++- apps/backend/src/routes/users/route.ts | 135 +++++++++--------- apps/backend/src/types/HonoEnv.d.ts | 8 ++ 6 files changed, 181 insertions(+), 75 deletions(-) create mode 100644 apps/backend/src/middlewares/authInfo.ts delete mode 100644 apps/backend/src/middlewares/authMiddleware.ts create mode 100644 apps/backend/src/middlewares/checkPermission.ts diff --git a/apps/backend/src/middlewares/authInfo.ts b/apps/backend/src/middlewares/authInfo.ts new file mode 100644 index 0000000..d035c0a --- /dev/null +++ b/apps/backend/src/middlewares/authInfo.ts @@ -0,0 +1,74 @@ +import { createMiddleware } from "hono/factory"; +import HonoEnv from "../types/HonoEnv"; +import db from "../drizzle"; +import { users } from "../drizzle/schema/users"; +import { permissionsToUsers } from "../drizzle/schema/permissionsToUsers"; +import { rolesToUsers } from "../drizzle/schema/rolesToUsers"; +import { and, eq, isNull, or } from "drizzle-orm"; +import { rolesSchema } from "../drizzle/schema/roles"; +import { permissionsToRoles } from "../drizzle/schema/permissionsToRoles"; +import { permissionsSchema } from "../drizzle/schema/permissions"; +import { RoleCode } from "../data/roles"; +import { SpecificPermissionCode } from "../data/permissions"; + +const authInfo = createMiddleware(async (c, next) => { + const { uid, currentUser } = c.var; + + if (uid && !currentUser) { + const user = await db + .select() + .from(users) + .where( + and( + eq(users.id, uid), + eq(users.isEnabled, true), + isNull(users.deletedAt) + ) + ) + .leftJoin( + permissionsToUsers, + eq(permissionsToUsers.userId, users.id) + ) + .leftJoin(rolesToUsers, eq(rolesToUsers.userId, users.id)) + .leftJoin(rolesSchema, eq(rolesToUsers.roleId, rolesSchema.id)) + .leftJoin( + permissionsToRoles, + eq(permissionsToRoles.roleId, rolesSchema.id) + ) + .leftJoin( + permissionsSchema, + or( + eq(permissionsSchema.id, permissionsToUsers.permissionId), + eq(permissionsSchema.id, permissionsToRoles.permissionId) + ) + ); + + const roles = new Set(); + const permissions = new Set(); + + user.forEach((user) => { + if (user.roles?.code) { + roles.add(user.roles.code as RoleCode); + } + + if (user.permissions?.code) { + permissions.add( + user.permissions.code as SpecificPermissionCode + ); + } + }); + + console.log("roles", Array.from(roles)); + console.log("permissions", Array.from(permissions)); + + c.set("currentUser", { + name: user[0].users.name, + permissions: Array.from(permissions), + roles: Array.from(roles), + }); + } + + await next(); +}); + +export default authInfo; diff --git a/apps/backend/src/middlewares/authMiddleware.ts b/apps/backend/src/middlewares/authMiddleware.ts deleted file mode 100644 index e69de29..0000000 diff --git a/apps/backend/src/middlewares/checkPermission.ts b/apps/backend/src/middlewares/checkPermission.ts new file mode 100644 index 0000000..8243db1 --- /dev/null +++ b/apps/backend/src/middlewares/checkPermission.ts @@ -0,0 +1,21 @@ +import { createMiddleware } from "hono/factory"; +import { PermissionCode } from "../data/permissions"; +import HonoEnv from "../types/HonoEnv"; +import { unauthorized } from "../errors/DashboardError"; + +const checkPermission = (...permissions: PermissionCode[]) => + createMiddleware(async (c, next) => { + if (permissions.includes("*")) await next(); + else if (c.var.currentUser) { + if ( + c.var.currentUser.permissions.some((p) => + permissions.includes(p) + ) + ) + await next(); + } else { + unauthorized(); + } + }); + +export default checkPermission; diff --git a/apps/backend/src/routes/dev/route.ts b/apps/backend/src/routes/dev/route.ts index 6a9eb31..d54ec3a 100644 --- a/apps/backend/src/routes/dev/route.ts +++ b/apps/backend/src/routes/dev/route.ts @@ -1,16 +1,14 @@ import { Hono } from "hono"; import DashboardError from "../../errors/DashboardError"; +import authInfo from "../../middlewares/authInfo"; +import HonoEnv from "../../types/HonoEnv"; -const devRoutes = new Hono().get("/error", async () => { - throw new DashboardError({ - errorCode: "TEST_ERROR", - message: "Test error", - severity: "LOW", - statusCode: 400, - formErrors: { - someField: "error", - }, +const devRoutes = new Hono() + .use(authInfo) + .get("/middleware", async (c) => { + return c.json({ + message: "Middleware works!", + }); }); -}); export default devRoutes; diff --git a/apps/backend/src/routes/users/route.ts b/apps/backend/src/routes/users/route.ts index c4ed8a3..650319e 100644 --- a/apps/backend/src/routes/users/route.ts +++ b/apps/backend/src/routes/users/route.ts @@ -10,6 +10,8 @@ import { rolesToUsers } from "../../drizzle/schema/rolesToUsers"; import { rolesSchema } from "../../drizzle/schema/roles"; import HonoEnv from "../../types/HonoEnv"; import requestValidator from "../../utils/requestValidator"; +import authInfo from "../../middlewares/authInfo"; +import checkPermission from "../../middlewares/checkPermission"; const userFormSchema = z.object({ name: z.string().min(1).max(255), @@ -41,19 +43,10 @@ const userUpdateSchema = userFormSchema.extend({ }); const usersRoute = new Hono() - .use(async (c, next) => { - const uid = c.get("uid"); - - if (uid) { - await next(); - } else { - throw new HTTPException(401, { - message: "Unauthorized", - }); - } - }) + .use(authInfo) .get( "/", + checkPermission("users.readAll"), requestValidator( "query", z.object({ @@ -86,6 +79,7 @@ const usersRoute = new Hono() //get user by id .get( "/:id", + checkPermission("users.readAll"), requestValidator( "query", z.object({ @@ -144,74 +138,85 @@ const usersRoute = new Hono() } ) //create user - .post("/", requestValidator("form", userFormSchema), async (c) => { - const userData = c.req.valid("form"); + .post( + "/", + checkPermission("users.create"), + requestValidator("form", userFormSchema), + async (c) => { + const userData = c.req.valid("form"); - 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(); + 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 (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, - })) - ); + if (roles.length) { + await db.insert(rolesToUsers).values( + roles.map((role) => ({ + userId: user[0].id, + roleId: role, + })) + ); + } } - } - return c.json( - { - message: "User created successfully", - }, - 201 - ); - }) + return c.json( + { + message: "User created successfully", + }, + 201 + ); + } + ) //update user - .patch("/:id", requestValidator("form", userUpdateSchema), async (c) => { - const userId = c.req.param("id"); - const userData = c.req.valid("form"); + .patch( + "/:id", + checkPermission("users.update"), + requestValidator("form", userUpdateSchema), + async (c) => { + const userId = c.req.param("id"); + const userData = c.req.valid("form"); - const user = await db - .select() - .from(users) - .where(and(eq(users.id, userId), isNull(users.deletedAt))); + const user = await db + .select() + .from(users) + .where(and(eq(users.id, userId), isNull(users.deletedAt))); - if (!user[0]) return c.notFound(); + 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)); + 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)); - return c.json({ - message: "User updated successfully", - }); - }) + return c.json({ + message: "User updated successfully", + }); + } + ) //delete user .delete( "/:id", + checkPermission("users.delete"), requestValidator( "form", z.object({ @@ -263,7 +268,7 @@ const usersRoute = new Hono() ) //undo delete - .patch("/restore/:id", async (c) => { + .patch("/restore/:id", checkPermission("users.restore"), async (c) => { const userId = c.req.param("id"); const user = ( diff --git a/apps/backend/src/types/HonoEnv.d.ts b/apps/backend/src/types/HonoEnv.d.ts index e7f3d72..4cf35db 100644 --- a/apps/backend/src/types/HonoEnv.d.ts +++ b/apps/backend/src/types/HonoEnv.d.ts @@ -1,6 +1,14 @@ +import { SpecificPermissionCode } from "../data/permissions"; +import { RoleCode } from "../data/roles"; + type HonoEnv = { Variables: { uid?: string; + currentUser?: { + name: string; + permissions: SpecificPermissionCode[]; + roles: RoleCode[]; + }; }; };