Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
PORT=5000
MONGO_URI=mongodb://root:admin@localhost:27017/
JWT_SECRET=your_jwt_secret_here
RESEND_API_KEY=""
CORS_URL=http://localhost:5173
3,598 changes: 3,384 additions & 214 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"express-async-handler": "^1.2.0",
"express-rate-limiter": "^1.3.1",
"joi": "^18.0.1",
"jsonwebtoken": "^9.0.2",
Expand Down
4 changes: 3 additions & 1 deletion src/app.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import express from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
Expand All @@ -8,6 +7,7 @@ import dotenv from "dotenv";
import roleRoutes from "./routes/role.routes.js";
import permissionRoutes from "./routes/permission.routes.js";
import rateLimiter from './middlewares/rateLimiter.js';
import errorHandler from "./middlewares/error.middleware.js";

dotenv.config();

Expand Down Expand Up @@ -37,5 +37,7 @@ app.use('/api/rbac-test', rbacRoutes);
app.get("/", (req, res) => {
res.send("RBAC is running...");
});
//global error handler
app.use(errorHandler);

export { app };
144 changes: 53 additions & 91 deletions src/controllers/authController.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,48 @@ import { User } from '../models/user.model.js';
import { registerUserService, loginUserService } from '../services/authService.js';
import jwt from 'jsonwebtoken'
import { sendEmail } from '../utils/sendEmail.js';

export const registerUser = async (req, res) => {
try {
import asynkcHandler from 'express-async-handler';
import ApiError from '../utils/ApiError.js';
//register user
export const registerUser = asynkcHandler(async (req, res) => {
const userData = await registerUserService(req.body);
return res.status(201).json({
success: true,
message: 'User registered successfully',
user: userData
});
} catch (error) {
console.error('Error in registerUser:', error.message);
res.status(400).json({ success: false, message: error.message });
}
};
});
// login user
export const loginUser = asynkcHandler(async (req, res) => {
const { email, password } = req.body;

const result = await loginUserService({ email, password });

res.status(200).json({
success: true,
message: 'Login successful',
accessToken: result.accessToken,
refreshToken: result.refreshToken,
user: result.user,
});
});
// Forgot password
export const forgotPassword = asynkcHandler(async (req, res) => {
const { email } = req.body;

export const loginUser = async (req, res) => {
try {
const { email, password } = req.body;

const result = await loginUserService({ email, password });
const user = await User.findOne({ email });

return res.status(200).json({
success: true,
message: 'Login successful',
accessToken: result.accessToken,
refreshToken: result.refreshToken,
user: result.user,
});
} catch (error) {
console.error('Error in loginUser:', error);
const status = error.statusCode || 400;
return res.status(status).json({ success: false, message: error.message || 'Login failed' });
if (!user) {
throw new ApiError(401, 'User not found');
}
};

export const forgotPassword = async (req, res) => {
const { email } = req.body;

try {
const user = await User.findOne({ email });

if (!user) {
return res.status(401).json({
success: false,
message: "User not found"
})
const resetToken = jwt.sign(
{ id: user._id },
process.env.JWT_SECRET,
{
expiresIn: '1h'
}

const resetToken = jwt.sign(
{ id: user._id },
process.env.JWT_SECRET,
{
expiresIn: '1h'
}
);
);


user.refreshToken = resetToken;
Expand All @@ -79,56 +66,31 @@ export const forgotPassword = async (req, res) => {
success: true,
message: 'Password reset link sent'
})
} catch (error) {
console.error(error)
return res.status(500).json({
success: false,
message: 'server error'
})

});
// Reset password
export const resetPassword = asynkcHandler(async (req, res) => {
const { token } = req.params;
const { password } = req.body;

if (!password) {
throw new ApiError(400, 'Password is required');
}
}


export const resetPassword = async (req, res) => {
let decoded;
try {
const { token } = req.params;
const { password } = req.body;

if (!password) {
return res.status(400).json({
success: false,
message: 'Password is required'
})
}

let decoded;
try {
decoded = jwt.verify(token, process.env.JWT_SECRET)
} catch (error) {

return res.status(400).json({
success: false,
message: 'Invalid or expired token'
})
}



const user = await User.findById(decoded.id);
console.log(user)
if (!user || user.refreshToken !== token) {
return res.status(400).json({ success: false, message: 'Invalid or expired token' });
}
user.password = password;


user.refreshToken = undefined;
await user.save();
return res.status(200).json({ success: true, message: 'Password reset successful' });
decoded = jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
throw new ApiError(400, 'Invalid or expired token');
}

return res.status(500).json({ success: false, message: 'Server error' });

const user = await User.findById(decoded.id);
if (!user || user.refreshToken !== token) {
throw new ApiError(400, 'Invalid or expired token');
}
}

user.password = password;
user.refreshToken = undefined;
await user.save();

res.status(200).json({ success: true, message: 'Password reset successful' });
});
70 changes: 26 additions & 44 deletions src/controllers/permission.controller.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,32 @@
import asyncHandler from 'express-async-handler';
import * as permissionService from "../services/permission.service.js";
import ApiError from '../utils/ApiError.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 createPermission = asyncHandler(async (req, res) => {
const { name, description } = req.body;
const perm = await permissionService.createPermission(name, description);
res.status(201).json(perm);
});

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 getPermissions = asyncHandler(async (req, res) => {
const perms = await permissionService.getPermissions();
res.json(perms);
});

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 getPermissionById = asyncHandler(async (req, res) => {
const perm = await permissionService.getPermissionById(req.params.id);
if (!perm) throw new ApiError(404, 'Permission not found');
res.json(perm);
});

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 updatePermission = asyncHandler(async (req, res) => {
const perm = await permissionService.updatePermission(req.params.id, req.body);
if (!perm) throw new ApiError(404, 'Permission not found');
res.json(perm);
});

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 });
}
};
export const deletePermission = asyncHandler(async (req, res) => {
const deleted = await permissionService.deletePermission(req.params.id);
if (!deleted) throw new ApiError(404, 'Permission not found');
res.json({ message: "Permission deleted successfully" });
});
16 changes: 10 additions & 6 deletions src/middlewares/auth.middleware.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
import jwt from "jsonwebtoken";
import { User } from "../models/user.model.js";
import Role from "../models/Role.model.js";
import ApiError from "../utils/ApiError.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" });
if (!token) return next(new ApiError(401, '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" });
if (!user) return next(new ApiError(404, 'User not found'));

req.user = user;
next();
} catch (error) {
return res.status(401).json({ message: "Invalid or expired token" });
if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') {
return next(new ApiError(401, 'Invalid or expired token'));
}
return next(new ApiError(500, 'Error in authentication middleware'));
}
};

export const rbacMiddleware = (requiredRole) => {
return async (req, res, next) => {
try {
if (!req.user?.role) {
return res.status(403).json({ message: "Role not assigned" });
return next(new ApiError(403, 'Role not assigned'));
}
let userRole;
if (typeof req.user.role === "object" && req.user.role !== null) {
Expand All @@ -34,12 +38,12 @@ export const rbacMiddleware = (requiredRole) => {
}

if (!userRole || userRole.name !== requiredRole) {
return res.status(403).json({ message: "Access denied" });
return next(new ApiError(403, 'Access denied'));
}

next();
} catch (error) {
return res.status(500).json({ message: "Error in RBAC middleware" });
return next(new ApiError(500, 'Error in RBAC middleware'));
}
};
};
20 changes: 20 additions & 0 deletions src/middlewares/error.middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import ApiError from '../utils/ApiError.js';

export default function errorHandler(err, req, res, next) {
const statusCode = err.statusCode || err.status || 500;
const message = err.message || 'Internal Server Error';

console.error(err);

const response = {
success: false,
message,
};
if (err.details) {
response.details = err.details;
}
if (process.env.NODE_ENV === 'development') {
response.stack = err.stack;
}
res.status(statusCode).json(response);
}
8 changes: 6 additions & 2 deletions src/middlewares/rateLimiter.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { MAX_REQUESTS, WINDOW_MS } from '../config/rateLimiter.js';

import ApiError from '../utils/ApiError.js';
// Basic in-memory store
const requests = {};

export default function rateLimiter(req, res, next) {
try{
const now = Date.now();
const ip = req.ip;

Expand All @@ -13,9 +14,12 @@ export default function rateLimiter(req, res, next) {
requests[ip] = requests[ip].filter(ts => now - ts < WINDOW_MS);

if (requests[ip].length >= MAX_REQUESTS) {
return res.status(429).send('Too many requests, please try again later.');
return next(new ApiError(429, 'Too many requests, please try again later.'));
}

requests[ip].push(now);
next();
} catch (error) {
next(new ApiError(500, 'Rate limiter error'));
}
}
Loading
Loading