Skip to content

Commit 1e0fd4d

Browse files
authored
Merge pull request #40 from indrasuthar07/feat/errorhandler
feat: implement centralized error handling middleware
2 parents 5bc74d2 + 5a30028 commit 1e0fd4d

12 files changed

Lines changed: 3531 additions & 375 deletions

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22
PORT=5000
33
MONGO_URI=mongodb://root:admin@localhost:27017/
44
JWT_SECRET=your_jwt_secret_here
5+
RESEND_API_KEY=""
6+
CORS_URL=http://localhost:5173

package-lock.json

Lines changed: 3384 additions & 214 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"cors": "^2.8.5",
2424
"dotenv": "^17.2.3",
2525
"express": "^5.1.0",
26+
"express-async-handler": "^1.2.0",
2627
"express-rate-limiter": "^1.3.1",
2728
"joi": "^18.0.1",
2829
"jsonwebtoken": "^9.0.2",

src/app.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import express from "express";
32
import cors from "cors";
43
import cookieParser from "cookie-parser";
@@ -8,6 +7,7 @@ import dotenv from "dotenv";
87
import roleRoutes from "./routes/role.routes.js";
98
import permissionRoutes from "./routes/permission.routes.js";
109
import rateLimiter from './middlewares/rateLimiter.js';
10+
import errorHandler from "./middlewares/error.middleware.js";
1111

1212
dotenv.config();
1313

@@ -37,5 +37,7 @@ app.use('/api/rbac-test', rbacRoutes);
3737
app.get("/", (req, res) => {
3838
res.send("RBAC is running...");
3939
});
40+
//global error handler
41+
app.use(errorHandler);
4042

4143
export { app };

src/controllers/authController.js

Lines changed: 53 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -2,61 +2,48 @@ import { User } from '../models/user.model.js';
22
import { registerUserService, loginUserService } from '../services/authService.js';
33
import jwt from 'jsonwebtoken'
44
import { sendEmail } from '../utils/sendEmail.js';
5-
6-
export const registerUser = async (req, res) => {
7-
try {
5+
import asynkcHandler from 'express-async-handler';
6+
import ApiError from '../utils/ApiError.js';
7+
//register user
8+
export const registerUser = asynkcHandler(async (req, res) => {
89
const userData = await registerUserService(req.body);
910
return res.status(201).json({
1011
success: true,
1112
message: 'User registered successfully',
1213
user: userData
1314
});
14-
} catch (error) {
15-
console.error('Error in registerUser:', error.message);
16-
res.status(400).json({ success: false, message: error.message });
17-
}
18-
};
15+
});
16+
// login user
17+
export const loginUser = asynkcHandler(async (req, res) => {
18+
const { email, password } = req.body;
19+
20+
const result = await loginUserService({ email, password });
21+
22+
res.status(200).json({
23+
success: true,
24+
message: 'Login successful',
25+
accessToken: result.accessToken,
26+
refreshToken: result.refreshToken,
27+
user: result.user,
28+
});
29+
});
30+
// Forgot password
31+
export const forgotPassword = asynkcHandler(async (req, res) => {
32+
const { email } = req.body;
1933

20-
export const loginUser = async (req, res) => {
21-
try {
22-
const { email, password } = req.body;
23-
24-
const result = await loginUserService({ email, password });
34+
const user = await User.findOne({ email });
2535

26-
return res.status(200).json({
27-
success: true,
28-
message: 'Login successful',
29-
accessToken: result.accessToken,
30-
refreshToken: result.refreshToken,
31-
user: result.user,
32-
});
33-
} catch (error) {
34-
console.error('Error in loginUser:', error);
35-
const status = error.statusCode || 400;
36-
return res.status(status).json({ success: false, message: error.message || 'Login failed' });
36+
if (!user) {
37+
throw new ApiError(401, 'User not found');
3738
}
38-
};
39-
40-
export const forgotPassword = async (req, res) => {
41-
const { email } = req.body;
4239

43-
try {
44-
const user = await User.findOne({ email });
45-
46-
if (!user) {
47-
return res.status(401).json({
48-
success: false,
49-
message: "User not found"
50-
})
40+
const resetToken = jwt.sign(
41+
{ id: user._id },
42+
process.env.JWT_SECRET,
43+
{
44+
expiresIn: '1h'
5145
}
52-
53-
const resetToken = jwt.sign(
54-
{ id: user._id },
55-
process.env.JWT_SECRET,
56-
{
57-
expiresIn: '1h'
58-
}
59-
);
46+
);
6047

6148

6249
user.refreshToken = resetToken;
@@ -79,56 +66,31 @@ export const forgotPassword = async (req, res) => {
7966
success: true,
8067
message: 'Password reset link sent'
8168
})
82-
} catch (error) {
83-
console.error(error)
84-
return res.status(500).json({
85-
success: false,
86-
message: 'server error'
87-
})
88-
69+
});
70+
// Reset password
71+
export const resetPassword = asynkcHandler(async (req, res) => {
72+
const { token } = req.params;
73+
const { password } = req.body;
74+
75+
if (!password) {
76+
throw new ApiError(400, 'Password is required');
8977
}
90-
}
91-
9278

93-
export const resetPassword = async (req, res) => {
79+
let decoded;
9480
try {
95-
const { token } = req.params;
96-
const { password } = req.body;
97-
98-
if (!password) {
99-
return res.status(400).json({
100-
success: false,
101-
message: 'Password is required'
102-
})
103-
}
104-
105-
let decoded;
106-
try {
107-
decoded = jwt.verify(token, process.env.JWT_SECRET)
108-
} catch (error) {
109-
110-
return res.status(400).json({
111-
success: false,
112-
message: 'Invalid or expired token'
113-
})
114-
}
115-
116-
117-
118-
const user = await User.findById(decoded.id);
119-
console.log(user)
120-
if (!user || user.refreshToken !== token) {
121-
return res.status(400).json({ success: false, message: 'Invalid or expired token' });
122-
}
123-
user.password = password;
124-
125-
126-
user.refreshToken = undefined;
127-
await user.save();
128-
return res.status(200).json({ success: true, message: 'Password reset successful' });
81+
decoded = jwt.verify(token, process.env.JWT_SECRET);
12982
} catch (error) {
83+
throw new ApiError(400, 'Invalid or expired token');
84+
}
13085

131-
return res.status(500).json({ success: false, message: 'Server error' });
132-
86+
const user = await User.findById(decoded.id);
87+
if (!user || user.refreshToken !== token) {
88+
throw new ApiError(400, 'Invalid or expired token');
13389
}
134-
}
90+
91+
user.password = password;
92+
user.refreshToken = undefined;
93+
await user.save();
94+
95+
res.status(200).json({ success: true, message: 'Password reset successful' });
96+
});
Lines changed: 26 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,32 @@
1+
import asyncHandler from 'express-async-handler';
12
import * as permissionService from "../services/permission.service.js";
3+
import ApiError from '../utils/ApiError.js';
24

3-
export const createPermission = async (req, res) => {
4-
try {
5-
const { name, description } = req.body;
6-
const perm = await permissionService.createPermission(name, description);
7-
res.status(201).json(perm);
8-
} catch (err) {
9-
res.status(400).json({ message: err.message });
10-
}
11-
};
5+
export const createPermission = asyncHandler(async (req, res) => {
6+
const { name, description } = req.body;
7+
const perm = await permissionService.createPermission(name, description);
8+
res.status(201).json(perm);
9+
});
1210

13-
export const getPermissions = async (req, res) => {
14-
try {
15-
const perms = await permissionService.getPermissions();
16-
res.json(perms);
17-
} catch (err) {
18-
res.status(500).json({ message: err.message });
19-
}
20-
};
11+
export const getPermissions = asyncHandler(async (req, res) => {
12+
const perms = await permissionService.getPermissions();
13+
res.json(perms);
14+
});
2115

22-
export const getPermissionById = async (req, res) => {
23-
try {
24-
const perm = await permissionService.getPermissionById(req.params.id);
25-
if (!perm) return res.status(404).json({ message: "Permission not found" });
26-
res.json(perm);
27-
} catch (err) {
28-
res.status(500).json({ message: err.message });
29-
}
30-
};
16+
export const getPermissionById = asyncHandler(async (req, res) => {
17+
const perm = await permissionService.getPermissionById(req.params.id);
18+
if (!perm) throw new ApiError(404, 'Permission not found');
19+
res.json(perm);
20+
});
3121

32-
export const updatePermission = async (req, res) => {
33-
try {
34-
const perm = await permissionService.updatePermission(req.params.id, req.body);
35-
if (!perm) return res.status(404).json({ message: "Permission not found" });
36-
res.json(perm);
37-
} catch (err) {
38-
res.status(400).json({ message: err.message });
39-
}
40-
};
22+
export const updatePermission = asyncHandler(async (req, res) => {
23+
const perm = await permissionService.updatePermission(req.params.id, req.body);
24+
if (!perm) throw new ApiError(404, 'Permission not found');
25+
res.json(perm);
26+
});
4127

42-
export const deletePermission = async (req, res) => {
43-
try {
44-
const deleted = await permissionService.deletePermission(req.params.id);
45-
if (!deleted) return res.status(404).json({ message: "Permission not found" });
46-
res.json({ message: "Permission deleted successfully" });
47-
} catch (err) {
48-
res.status(500).json({ message: err.message });
49-
}
50-
};
28+
export const deletePermission = asyncHandler(async (req, res) => {
29+
const deleted = await permissionService.deletePermission(req.params.id);
30+
if (!deleted) throw new ApiError(404, 'Permission not found');
31+
res.json({ message: "Permission deleted successfully" });
32+
});

src/middlewares/auth.middleware.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,34 @@
11
import jwt from "jsonwebtoken";
22
import { User } from "../models/user.model.js";
33
import Role from "../models/Role.model.js";
4+
import ApiError from "../utils/ApiError.js";
45

56
export const authMiddleware = async (req, res, next) => {
67
try {
78
const token = req.headers.authorization?.split(" ")[1];
8-
if (!token) return res.status(401).json({ message: "No token provided" });
9+
if (!token) return next(new ApiError(401, 'No token provided'));
910

1011
const decoded = jwt.verify(token, process.env.JWT_SECRET);
1112
// populate role for convenience
1213
const user = await User.findById(decoded._id).populate("role");
1314

14-
if (!user) return res.status(404).json({ message: "User not found" });
15+
if (!user) return next(new ApiError(404, 'User not found'));
1516

1617
req.user = user;
1718
next();
1819
} catch (error) {
19-
return res.status(401).json({ message: "Invalid or expired token" });
20+
if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') {
21+
return next(new ApiError(401, 'Invalid or expired token'));
22+
}
23+
return next(new ApiError(500, 'Error in authentication middleware'));
2024
}
2125
};
2226

2327
export const rbacMiddleware = (requiredRole) => {
2428
return async (req, res, next) => {
2529
try {
2630
if (!req.user?.role) {
27-
return res.status(403).json({ message: "Role not assigned" });
31+
return next(new ApiError(403, 'Role not assigned'));
2832
}
2933
let userRole;
3034
if (typeof req.user.role === "object" && req.user.role !== null) {
@@ -34,12 +38,12 @@ export const rbacMiddleware = (requiredRole) => {
3438
}
3539

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

4044
next();
4145
} catch (error) {
42-
return res.status(500).json({ message: "Error in RBAC middleware" });
46+
return next(new ApiError(500, 'Error in RBAC middleware'));
4347
}
4448
};
4549
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import ApiError from '../utils/ApiError.js';
2+
3+
export default function errorHandler(err, req, res, next) {
4+
const statusCode = err.statusCode || err.status || 500;
5+
const message = err.message || 'Internal Server Error';
6+
7+
console.error(err);
8+
9+
const response = {
10+
success: false,
11+
message,
12+
};
13+
if (err.details) {
14+
response.details = err.details;
15+
}
16+
if (process.env.NODE_ENV === 'development') {
17+
response.stack = err.stack;
18+
}
19+
res.status(statusCode).json(response);
20+
}

src/middlewares/rateLimiter.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { MAX_REQUESTS, WINDOW_MS } from '../config/rateLimiter.js';
2-
2+
import ApiError from '../utils/ApiError.js';
33
// Basic in-memory store
44
const requests = {};
55

66
export default function rateLimiter(req, res, next) {
7+
try{
78
const now = Date.now();
89
const ip = req.ip;
910

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

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

1920
requests[ip].push(now);
2021
next();
22+
} catch (error) {
23+
next(new ApiError(500, 'Rate limiter error'));
24+
}
2125
}

0 commit comments

Comments
 (0)