From 4bf9da7061867cb547b3bf0c6ba4f3e9d7f16508 Mon Sep 17 00:00:00 2001 From: indar suthar Date: Tue, 14 Oct 2025 19:44:09 +0530 Subject: [PATCH] feat(admin): implement CRUD endpoints for roles and permission --- src/app.js | 37 ++++++++----- src/controllers/permission.controller.js | 50 ++++++++++++++++++ src/controllers/role.controller.js | 66 ++++++++++++++++++++++++ src/middlewares/auth.middleware.js | 45 ++++++++++++++++ src/routes/permission.routes.js | 18 +++++++ src/routes/role.routes.js | 20 +++++++ src/services/permission.service.js | 22 ++++++++ src/services/role.service.js | 29 +++++++++++ 8 files changed, 275 insertions(+), 12 deletions(-) create mode 100644 src/controllers/permission.controller.js create mode 100644 src/controllers/role.controller.js create mode 100644 src/middlewares/auth.middleware.js create mode 100644 src/routes/permission.routes.js create mode 100644 src/routes/role.routes.js create mode 100644 src/services/permission.service.js create mode 100644 src/services/role.service.js diff --git a/src/app.js b/src/app.js index 08050ce..6fd39a6 100644 --- a/src/app.js +++ b/src/app.js @@ -1,21 +1,34 @@ import express from "express"; -import cors from "cors" -import cookieparser from "cookie-parser" +import cors from "cors"; +import cookieParser from "cookie-parser"; import authRoutes from "./routes/authRoutes.js" +import dotenv from "dotenv"; +import roleRoutes from "./routes/role.routes.js"; +import permissionRoutes from "./routes/permission.routes.js"; +dotenv.config(); -const app = express() +const app = express(); -app.use(cors({ - origin: process.env.CORS_URL, - credentials: true -})) +// Middleware setup +app.use( + cors({ + origin: process.env.CORS_URL || "*", + credentials: true, + }) +); -app.use(express.json({ limit: "16kb" })) -app.use(express.urlencoded({ extended: true, limit: "16kb" })) -app.use(express.static("public")) -app.use(cookieparser()) +app.use(express.json({ limit: "16kb" })); +app.use(express.urlencoded({ extended: true, limit: "16kb" })); +app.use(express.static("public")); +app.use(cookieParser()); // Routes +app.use("/api/roles", roleRoutes); +app.use("/api/permissions", permissionRoutes); app.use("/api/auth", authRoutes) +// Root route +app.get("/", (req, res) => { + res.send("RBAC is running..."); +}); -export { app } \ No newline at end of file +export { app }; \ No newline at end of file diff --git a/src/controllers/permission.controller.js b/src/controllers/permission.controller.js new file mode 100644 index 0000000..c4ffefa --- /dev/null +++ b/src/controllers/permission.controller.js @@ -0,0 +1,50 @@ +import * as permissionService from "../services/permission.service.js"; + +export const createPermission = async (req, res) => { + try { + const { name, description } = req.body; + const perm = await permissionService.createPermission(name, description); + res.status(201).json(perm); + } catch (err) { + res.status(400).json({ message: err.message }); + } +}; + +export const getPermissions = async (req, res) => { + try { + const perms = await permissionService.getPermissions(); + res.json(perms); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; + +export const getPermissionById = async (req, res) => { + try { + const perm = await permissionService.getPermissionById(req.params.id); + if (!perm) return res.status(404).json({ message: "Permission not found" }); + res.json(perm); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; + +export const updatePermission = async (req, res) => { + try { + const perm = await permissionService.updatePermission(req.params.id, req.body); + if (!perm) return res.status(404).json({ message: "Permission not found" }); + res.json(perm); + } catch (err) { + res.status(400).json({ message: err.message }); + } +}; + +export const deletePermission = async (req, res) => { + try { + const deleted = await permissionService.deletePermission(req.params.id); + if (!deleted) return res.status(404).json({ message: "Permission not found" }); + res.json({ message: "Permission deleted successfully" }); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; \ No newline at end of file diff --git a/src/controllers/role.controller.js b/src/controllers/role.controller.js new file mode 100644 index 0000000..8753117 --- /dev/null +++ b/src/controllers/role.controller.js @@ -0,0 +1,66 @@ +import * as roleService from "../services/role.service.js"; + +export const createRole = async (req, res) => { + try { + const { name, permissions } = req.body; + const role = await roleService.createRole(name, permissions); + res.status(201).json(role); + } catch (err) { + res.status(400).json({ message: err.message }); + } +}; + +export const getRoles = async (req, res) => { + try { + const roles = await roleService.getRoles(); + res.json(roles); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; + +export const getRoleById = async (req, res) => { + try { + const role = await roleService.getRoleById(req.params.id); + if (!role) return res.status(404).json({ message: "Role not found" }); + res.json(role); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; + +export const updateRole = async (req, res) => { + try { + const role = await roleService.updateRole(req.params.id, req.body); + if (!role) return res.status(404).json({ message: "Role not found" }); + res.json(role); + } catch (err) { + res.status(400).json({ message: err.message }); + } +}; + +export const deleteRole = async (req, res) => { + try { + const deleted = await roleService.deleteRole(req.params.id); + if (!deleted) return res.status(404).json({ message: "Role not found" }); + res.json({ message: "Role deleted successfully" }); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; + +export const assignPermissions = async (req, res) => { + try { + const { permissions } = req.body; + if (!Array.isArray(permissions)) { + return res.status(400).json({ message: "permissions must be an array of permission IDs" }); + } + const role = await roleService.assignPermissions(req.params.id, permissions); + if (!role) return res.status(404).json({ message: "Role not found" }); + // populate permissions before returning + const populated = await roleService.getRoleById(role._id); + res.json(populated); + } catch (err) { + res.status(400).json({ message: err.message }); + } +}; \ No newline at end of file diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js new file mode 100644 index 0000000..a117950 --- /dev/null +++ b/src/middlewares/auth.middleware.js @@ -0,0 +1,45 @@ +import jwt from "jsonwebtoken"; +import { User } from "../models/user.model.js"; +import Role from "../models/Role.model.js"; + +export const authMiddleware = async (req, res, next) => { + try { + const token = req.headers.authorization?.split(" ")[1]; + if (!token) return res.status(401).json({ message: "No token provided" }); + + const decoded = jwt.verify(token, process.env.JWT_SECRET); + // populate role for convenience + const user = await User.findById(decoded._id).populate("role"); + + if (!user) return res.status(404).json({ message: "User not found" }); + + req.user = user; + next(); + } catch (error) { + return res.status(401).json({ message: "Invalid or expired token" }); + } +}; + +export const rbacMiddleware = (requiredRole) => { + return async (req, res, next) => { + try { + if (!req.user?.role) { + return res.status(403).json({ message: "Role not assigned" }); + } + let userRole; + if (typeof req.user.role === "object" && req.user.role !== null) { + userRole = req.user.role; + } else { + userRole = await Role.findById(req.user.role); + } + + if (!userRole || userRole.name !== requiredRole) { + return res.status(403).json({ message: "Access denied" }); + } + + next(); + } catch (error) { + return res.status(500).json({ message: "Error in RBAC middleware" }); + } + }; +}; \ No newline at end of file diff --git a/src/routes/permission.routes.js b/src/routes/permission.routes.js new file mode 100644 index 0000000..54216be --- /dev/null +++ b/src/routes/permission.routes.js @@ -0,0 +1,18 @@ +import express from "express"; +import { + createPermission, + getPermissions, + getPermissionById, + updatePermission, + deletePermission +} from "../controllers/permission.controller.js"; + +const router = express.Router(); + +router.post("/", createPermission); +router.get("/", getPermissions); +router.get("/:id", getPermissionById); +router.put("/:id", updatePermission); +router.delete("/:id", deletePermission); + +export default router; \ No newline at end of file diff --git a/src/routes/role.routes.js b/src/routes/role.routes.js new file mode 100644 index 0000000..6e01614 --- /dev/null +++ b/src/routes/role.routes.js @@ -0,0 +1,20 @@ +import express from "express"; +import { + createRole, + getRoles, + getRoleById, + updateRole, + deleteRole, + assignPermissions, +} from "../controllers/role.controller.js"; + +const router = express.Router(); + +router.post("/", createRole); +router.get("/", getRoles); +router.get("/:id", getRoleById); +router.put("/:id", updateRole); +router.delete("/:id", deleteRole); +router.put("/:id/permissions", assignPermissions); + +export default router; \ No newline at end of file diff --git a/src/services/permission.service.js b/src/services/permission.service.js new file mode 100644 index 0000000..0f52632 --- /dev/null +++ b/src/services/permission.service.js @@ -0,0 +1,22 @@ +import Permission from "../models/Permission.model.js"; + +export const createPermission = async (name, description) => { + const perm = new Permission({ name, description }); + return await perm.save(); +}; + +export const getPermissions = async () => { + return await Permission.find(); +}; + +export const getPermissionById = async (id) => { + return await Permission.findById(id); +}; + +export const updatePermission = async (id, data) => { + return await Permission.findByIdAndUpdate(id, data, { new: true }); +}; + +export const deletePermission = async (id) => { + return await Permission.findByIdAndDelete(id); +}; diff --git a/src/services/role.service.js b/src/services/role.service.js new file mode 100644 index 0000000..c4eb5e8 --- /dev/null +++ b/src/services/role.service.js @@ -0,0 +1,29 @@ +import Role from "../models/Role.model.js"; +import Permission from "../models/Permission.model.js"; + +export const createRole = async (name, permissions = []) => { + const role = new Role({ name, permissions }); + return await role.save(); +}; + +export const getRoles = async () => { + return await Role.find().populate("permissions"); +}; + +export const getRoleById = async (id) => { + return await Role.findById(id).populate("permissions"); +}; + +export const updateRole = async (id, data) => { + return await Role.findByIdAndUpdate(id, data, { new: true }); +}; + +export const deleteRole = async (id) => { + return await Role.findByIdAndDelete(id); +}; + +export const assignPermissions = async (roleId, permissionIds) => { + const role = await Role.findById(roleId); + role.permissions = permissionIds; + return await role.save(); +};