diff --git a/src/app.js b/src/app.js index 5c5e0d7..df35449 100644 --- a/src/app.js +++ b/src/app.js @@ -1,23 +1,36 @@ -import express from 'express'; -import cors from 'cors'; -import cookieparser from 'cookie-parser'; -import authRoutes from './routes/authRoutes.js'; +import express from "express"; +import cors from "cors"; +import cookieParser from "cookie-parser"; +import authRoutes from "./routes/authRoutes.js" import rbacRoutes from './routes/rbacRoutes.js'; +import dotenv from "dotenv"; +import roleRoutes from "./routes/role.routes.js"; +import permissionRoutes from "./routes/permission.routes.js"; +dotenv.config(); 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/auth', authRoutes); +// Routes +app.use("/api/roles", roleRoutes); +app.use("/api/permissions", permissionRoutes); +app.use("/api/auth", authRoutes) app.use('/api/rbac-test', rbacRoutes); +// Root route +app.get("/", (req, res) => { + res.send("RBAC is running..."); +}); 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 index 9b913f2..0d1c667 100644 --- a/src/middlewares/auth.middleware.js +++ b/src/middlewares/auth.middleware.js @@ -1,25 +1,45 @@ -import jwt from 'jsonwebtoken'; +import jwt from "jsonwebtoken"; +import { User } from "../models/user.model.js"; +import Role from "../models/Role.model.js"; -export const authMiddleware = (req, res, next) => { - const authHeader = req.headers.authorization; +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" }); - if (!authHeader || !authHeader.startsWith('Bearer')) { - return res.status(401).json({ - message: "Unauthorize : 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"); - const token = authHeader.split(" ")[1]; + if (!user) return res.status(404).json({ message: "User not found" }); - try { - const decoded = jwt.verify(token, process.env.JWT_SECRET); - - req.user = decoded; - next(); - } catch (error) { - console.error("Error: ", error.message); - return res.status(401).json({ message: "Unauthorized: Invalid token" }); - } -} + 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" }); + } + }; +}; 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(); +};