create-nodemin-app 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/bin/cli.js +82 -0
  2. package/package.json +25 -0
  3. package/templates/HRMS_Mongodb/README.md +331 -0
  4. package/templates/HRMS_Mongodb/backend/.env.example +6 -0
  5. package/templates/HRMS_Mongodb/backend/package-lock.json +1646 -0
  6. package/templates/HRMS_Mongodb/backend/package.json +26 -0
  7. package/templates/HRMS_Mongodb/backend/src/config/db.js +9 -0
  8. package/templates/HRMS_Mongodb/backend/src/controllers/authController.js +187 -0
  9. package/templates/HRMS_Mongodb/backend/src/controllers/departmentController.js +70 -0
  10. package/templates/HRMS_Mongodb/backend/src/controllers/employeeController.js +178 -0
  11. package/templates/HRMS_Mongodb/backend/src/controllers/positionController.js +66 -0
  12. package/templates/HRMS_Mongodb/backend/src/middleware/auth.js +57 -0
  13. package/templates/HRMS_Mongodb/backend/src/middleware/errorHandler.js +32 -0
  14. package/templates/HRMS_Mongodb/backend/src/middleware/restrictToAdmin.js +5 -0
  15. package/templates/HRMS_Mongodb/backend/src/middleware/validate.js +13 -0
  16. package/templates/HRMS_Mongodb/backend/src/models/Department.js +19 -0
  17. package/templates/HRMS_Mongodb/backend/src/models/Employee.js +81 -0
  18. package/templates/HRMS_Mongodb/backend/src/models/Position.js +19 -0
  19. package/templates/HRMS_Mongodb/backend/src/models/User.js +40 -0
  20. package/templates/HRMS_Mongodb/backend/src/routes/authRoutes.js +27 -0
  21. package/templates/HRMS_Mongodb/backend/src/routes/departmentRoutes.js +33 -0
  22. package/templates/HRMS_Mongodb/backend/src/routes/employeeRoutes.js +39 -0
  23. package/templates/HRMS_Mongodb/backend/src/routes/positionRoutes.js +32 -0
  24. package/templates/HRMS_Mongodb/backend/src/server.js +74 -0
  25. package/templates/HRMS_Mongodb/backend/src/utils/roles.js +5 -0
  26. package/templates/HRMS_Mongodb/backend/src/utils/seed.js +78 -0
  27. package/templates/HRMS_Mongodb/backend/src/validators/authValidator.js +61 -0
  28. package/templates/HRMS_Mongodb/backend/src/validators/departmentValidator.js +21 -0
  29. package/templates/HRMS_Mongodb/backend/src/validators/employeeValidator.js +27 -0
  30. package/templates/HRMS_Mongodb/backend/src/validators/positionValidator.js +26 -0
  31. package/templates/HRMS_Mongodb/frontend/index.html +19 -0
  32. package/templates/HRMS_Mongodb/frontend/package-lock.json +2812 -0
  33. package/templates/HRMS_Mongodb/frontend/package.json +25 -0
  34. package/templates/HRMS_Mongodb/frontend/public/favicon.svg +4 -0
  35. package/templates/HRMS_Mongodb/frontend/src/App.jsx +50 -0
  36. package/templates/HRMS_Mongodb/frontend/src/api/axios.js +54 -0
  37. package/templates/HRMS_Mongodb/frontend/src/components/ProtectedRoute.jsx +26 -0
  38. package/templates/HRMS_Mongodb/frontend/src/components/layout/DashboardLayout.jsx +16 -0
  39. package/templates/HRMS_Mongodb/frontend/src/components/layout/Sidebar.jsx +108 -0
  40. package/templates/HRMS_Mongodb/frontend/src/components/ui/Button.jsx +33 -0
  41. package/templates/HRMS_Mongodb/frontend/src/components/ui/Input.jsx +20 -0
  42. package/templates/HRMS_Mongodb/frontend/src/components/ui/Modal.jsx +48 -0
  43. package/templates/HRMS_Mongodb/frontend/src/components/ui/Select.jsx +27 -0
  44. package/templates/HRMS_Mongodb/frontend/src/context/AuthContext.jsx +97 -0
  45. package/templates/HRMS_Mongodb/frontend/src/index.css +34 -0
  46. package/templates/HRMS_Mongodb/frontend/src/main.jsx +16 -0
  47. package/templates/HRMS_Mongodb/frontend/src/pages/Dashboard.jsx +78 -0
  48. package/templates/HRMS_Mongodb/frontend/src/pages/Departments.jsx +144 -0
  49. package/templates/HRMS_Mongodb/frontend/src/pages/Employees.jsx +297 -0
  50. package/templates/HRMS_Mongodb/frontend/src/pages/LeaveReport.jsx +113 -0
  51. package/templates/HRMS_Mongodb/frontend/src/pages/Login.jsx +92 -0
  52. package/templates/HRMS_Mongodb/frontend/src/pages/Positions.jsx +157 -0
  53. package/templates/HRMS_Mongodb/frontend/src/pages/Register.jsx +93 -0
  54. package/templates/HRMS_Mongodb/frontend/src/pages/ResetPassword.jsx +135 -0
  55. package/templates/HRMS_Mongodb/frontend/src/utils/roles.js +1 -0
  56. package/templates/HRMS_Mongodb/frontend/src/utils/session.js +5 -0
  57. package/templates/HRMS_Mongodb/frontend/src/utils/validation.js +66 -0
  58. package/templates/HRMS_Mongodb/frontend/vite.config.js +16 -0
  59. package/templates/HRMS_Mysql/backend/db.js +13 -0
  60. package/templates/HRMS_Mysql/backend/package-lock.json +1614 -0
  61. package/templates/HRMS_Mysql/backend/package.json +21 -0
  62. package/templates/HRMS_Mysql/backend/server.js +421 -0
  63. package/templates/HRMS_Mysql/frontend/dist/assets/index-CtLtQf3_.js +75 -0
  64. package/templates/HRMS_Mysql/frontend/dist/assets/index-Dq1AXlEY.css +1 -0
  65. package/templates/HRMS_Mysql/frontend/dist/index.html +14 -0
  66. package/templates/HRMS_Mysql/frontend/dist/vite.svg +1 -0
  67. package/templates/HRMS_Mysql/frontend/index.html +13 -0
  68. package/templates/HRMS_Mysql/frontend/package-lock.json +2978 -0
  69. package/templates/HRMS_Mysql/frontend/package.json +25 -0
  70. package/templates/HRMS_Mysql/frontend/postcss.config.js +6 -0
  71. package/templates/HRMS_Mysql/frontend/public/vite.svg +1 -0
  72. package/templates/HRMS_Mysql/frontend/src/App.jsx +55 -0
  73. package/templates/HRMS_Mysql/frontend/src/api.js +11 -0
  74. package/templates/HRMS_Mysql/frontend/src/components/Layout.jsx +59 -0
  75. package/templates/HRMS_Mysql/frontend/src/index.css +7 -0
  76. package/templates/HRMS_Mysql/frontend/src/main.jsx +13 -0
  77. package/templates/HRMS_Mysql/frontend/src/pages/Dashboard.jsx +45 -0
  78. package/templates/HRMS_Mysql/frontend/src/pages/Departments.jsx +108 -0
  79. package/templates/HRMS_Mysql/frontend/src/pages/EmployeeStatusReport.jsx +72 -0
  80. package/templates/HRMS_Mysql/frontend/src/pages/Employees.jsx +252 -0
  81. package/templates/HRMS_Mysql/frontend/src/pages/ForgotPassword.jsx +66 -0
  82. package/templates/HRMS_Mysql/frontend/src/pages/Login.jsx +79 -0
  83. package/templates/HRMS_Mysql/frontend/src/pages/Positions.jsx +109 -0
  84. package/templates/HRMS_Mysql/frontend/src/pages/Register.jsx +95 -0
  85. package/templates/HRMS_Mysql/frontend/src/pages/Users.jsx +133 -0
  86. package/templates/HRMS_Mysql/frontend/tailwind.config.js +26 -0
  87. package/templates/HRMS_Mysql/frontend/vite.config.js +15 -0
  88. package/templates/HRMS_Mysql/hrms_schema.sql +57 -0
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "dab-hrms-backend",
3
+ "version": "1.0.0",
4
+ "description": "DAB Enterprise HRMS API",
5
+ "main": "src/server.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "start": "node src/server.js",
9
+ "dev": "nodemon src/server.js",
10
+ "seed": "node src/utils/seed.js"
11
+ },
12
+ "dependencies": {
13
+ "bcryptjs": "^2.4.3",
14
+ "cors": "^2.8.5",
15
+ "dotenv": "^16.4.7",
16
+ "express": "^4.21.2",
17
+ "express-mongo-sanitize": "^2.2.0",
18
+ "express-rate-limit": "^7.5.0",
19
+ "express-validator": "^7.2.1",
20
+ "helmet": "^8.0.0",
21
+ "jsonwebtoken": "^9.0.2",
22
+ "mongoose": "^8.9.3",
23
+ "nodemon": "^3.1.14",
24
+ "xss-clean": "^0.1.4"
25
+ }
26
+ }
@@ -0,0 +1,9 @@
1
+ import mongoose from 'mongoose';
2
+
3
+ const connectDB = async () => {
4
+ const uri = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/dab_hrms';
5
+ await mongoose.connect(uri);
6
+ console.log(`MongoDB connected: ${mongoose.connection.host}`);
7
+ };
8
+
9
+ export default connectDB;
@@ -0,0 +1,187 @@
1
+ import jwt from 'jsonwebtoken';
2
+ import User from '../models/User.js';
3
+ import Employee from '../models/Employee.js';
4
+ import Department from '../models/Department.js';
5
+ import Position from '../models/Position.js';
6
+
7
+ // --- Helper Functions ---
8
+
9
+ const signToken = (id) =>
10
+ jwt.sign({ id }, process.env.JWT_SECRET, {
11
+ expiresIn: process.env.JWT_EXPIRE || '7d',
12
+ });
13
+
14
+ const sendTokenResponse = (user, statusCode, res) => {
15
+ const token = signToken(user._id);
16
+ const userObj = user.toObject();
17
+ delete userObj.password;
18
+
19
+ res.status(statusCode).json({
20
+ success: true,
21
+ token,
22
+ user: userObj,
23
+ });
24
+ };
25
+
26
+ // --- Authentication Controllers ---
27
+
28
+ /**
29
+ * @desc Authenticate user & get token
30
+ * @route POST /api/auth/login
31
+ * @access Public
32
+ */
33
+ export const login = async (req, res, next) => {
34
+ try {
35
+ const { userName, password } = req.body;
36
+
37
+ const user = await User.findOne({ userName })
38
+ .select('+password')
39
+ .populate({
40
+ path: 'employee',
41
+ populate: [{ path: 'department' }, { path: 'position' }],
42
+ });
43
+
44
+ if (!user || !(await user.comparePassword(password))) {
45
+ return res.status(401).json({ success: false, message: 'Invalid username or password' });
46
+ }
47
+
48
+ sendTokenResponse(user, 200, res);
49
+ } catch (error) {
50
+ next(error);
51
+ }
52
+ };
53
+
54
+ /**
55
+ * @desc Register a new user and employee profile
56
+ * @route POST /api/auth/register
57
+ * @access Public
58
+ */
59
+ export const register = async (req, res, next) => {
60
+ try {
61
+ const { userName, password, email } = req.body;
62
+ const formattedEmail = email.toLowerCase().trim();
63
+
64
+ const existingUser = await User.findOne({ userName });
65
+ if (existingUser) {
66
+ return res.status(400).json({ success: false, message: 'Username is already taken' });
67
+ }
68
+
69
+ const existingEmail = await Employee.findOne({ empEmail: formattedEmail });
70
+ if (existingEmail) {
71
+ return res.status(400).json({ success: false, message: 'An account with this email already exists' });
72
+ }
73
+
74
+ let department = await Department.findOne().sort({ departName: 1 });
75
+ if (!department) {
76
+ department = await Department.create({
77
+ departName: 'General Administration',
78
+ departname: 'General Administration'
79
+ });
80
+ }
81
+
82
+ let position = await Position.findOne().sort({ posName: 1 });
83
+ if (!position) {
84
+ position = await Position.create({
85
+ posName: 'Staff Member',
86
+ posname: 'Staff Member',
87
+ RequiredQualification: 'Not Specified',
88
+ requiredQualification: 'Not Specified'
89
+ });
90
+ }
91
+
92
+ const employee = await Employee.create({
93
+ empFirstName: userName,
94
+ empLastName: 'Employee',
95
+ empEmail: formattedEmail,
96
+ empTelephone: 'N/A',
97
+ empGender: 'Other',
98
+ empAddress: 'Kigali, Rwanda',
99
+ empDateOfBirth: new Date('1995-01-01'),
100
+ empHireDate: new Date(),
101
+ empStatus: 'Active',
102
+ department: department._id,
103
+ position: position._id,
104
+ });
105
+
106
+ await User.create({
107
+ userName,
108
+ password,
109
+ employee: employee._id,
110
+ });
111
+
112
+ res.status(201).json({
113
+ success: true,
114
+ message: 'Your account has been created. You can now sign in.',
115
+ });
116
+ } catch (error) {
117
+ next(error);
118
+ }
119
+ };
120
+
121
+ /**
122
+ * @desc Custom User-defined Password Provision (Reset Flow)
123
+ * @route POST /api/auth/reset-password
124
+ * @access Public
125
+ */
126
+ export const resetPassword = async (req, res, next) => {
127
+ try {
128
+ const { email, password, confirmPassword } = req.body;
129
+
130
+ // 1. Core Defensively Safe Parameter Checks (Prevents uncaught crashes changing to 404s)
131
+ if (!email) {
132
+ return res.status(400).json({ success: false, message: 'Email field is empty or format was corrupted' });
133
+ }
134
+
135
+ if (!password || !confirmPassword) {
136
+ return res.status(400).json({ success: false, message: 'Please provide and confirm your new password' });
137
+ }
138
+
139
+ if (password !== confirmPassword) {
140
+ return res.status(400).json({ success: false, message: 'Passwords do not match' });
141
+ }
142
+
143
+ if (password.length < 6) {
144
+ return res.status(400).json({ success: false, message: 'Password must be at least 6 characters long' });
145
+ }
146
+
147
+ // Convert safely now that we verified the email variable string exists
148
+ const formattedEmail = String(email).toLowerCase().trim();
149
+
150
+ // 2. Query and link database profiles
151
+ const employee = await Employee.findOne({ empEmail: formattedEmail });
152
+ if (!employee) {
153
+ return res.status(404).json({
154
+ success: false,
155
+ message: 'No account found with this email address',
156
+ });
157
+ }
158
+
159
+ const user = await User.findOne({ employee: employee._id }).select('+password');
160
+ if (!user) {
161
+ return res.status(404).json({
162
+ success: false,
163
+ message: 'No user account linked to this employee email',
164
+ });
165
+ }
166
+
167
+ // 3. Mutate fields (triggers pre-save password encryption inside User schema automatically)
168
+ user.password = password;
169
+ await user.save();
170
+
171
+ res.status(200).json({
172
+ success: true,
173
+ message: 'Your password has been reset successfully. You can now use your new password to sign in.',
174
+ });
175
+ } catch (error) {
176
+ next(error);
177
+ }
178
+ };
179
+
180
+ /**
181
+ * @desc Get currently logged-in user profile details
182
+ * @route GET /api/auth/me
183
+ * @access Private
184
+ */
185
+ export const getMe = async (req, res) => {
186
+ res.status(200).json({ success: true, user: req.user });
187
+ };
@@ -0,0 +1,70 @@
1
+ import Department from '../models/Department.js';
2
+ import Employee from '../models/Employee.js';
3
+
4
+ export const getDepartments = async (req, res, next) => {
5
+ try {
6
+ const departments = await Department.find().sort({ departName: 1 });
7
+ res.status(200).json({ success: true, count: departments.length, data: departments });
8
+ } catch (error) {
9
+ next(error);
10
+ }
11
+ };
12
+
13
+ export const getDepartment = async (req, res, next) => {
14
+ try {
15
+ const department = await Department.findById(req.params.id);
16
+ if (!department) {
17
+ return res.status(404).json({ success: false, message: 'Department not found' });
18
+ }
19
+ const employees = await Employee.find({ department: department._id })
20
+ .populate('position')
21
+ .select('empFirstName empLastName empEmail empStatus');
22
+
23
+ res.status(200).json({ success: true, data: { ...department.toObject(), employees } });
24
+ } catch (error) {
25
+ next(error);
26
+ }
27
+ };
28
+
29
+ export const createDepartment = async (req, res, next) => {
30
+ try {
31
+ const department = await Department.create(req.body);
32
+ res.status(201).json({ success: true, data: department });
33
+ } catch (error) {
34
+ next(error);
35
+ }
36
+ };
37
+
38
+ export const updateDepartment = async (req, res, next) => {
39
+ try {
40
+ const department = await Department.findByIdAndUpdate(req.params.id, req.body, {
41
+ new: true,
42
+ runValidators: true,
43
+ });
44
+ if (!department) {
45
+ return res.status(404).json({ success: false, message: 'Department not found' });
46
+ }
47
+ res.status(200).json({ success: true, data: department });
48
+ } catch (error) {
49
+ next(error);
50
+ }
51
+ };
52
+
53
+ export const deleteDepartment = async (req, res, next) => {
54
+ try {
55
+ const count = await Employee.countDocuments({ department: req.params.id });
56
+ if (count > 0) {
57
+ return res.status(400).json({
58
+ success: false,
59
+ message: `Cannot delete department with ${count} assigned employee(s)`,
60
+ });
61
+ }
62
+ const department = await Department.findByIdAndDelete(req.params.id);
63
+ if (!department) {
64
+ return res.status(404).json({ success: false, message: 'Department not found' });
65
+ }
66
+ res.status(200).json({ success: true, message: 'Department deleted successfully' });
67
+ } catch (error) {
68
+ next(error);
69
+ }
70
+ };
@@ -0,0 +1,178 @@
1
+ import Employee from '../models/Employee.js';
2
+
3
+ export const getEmployees = async (req, res, next) => {
4
+ try {
5
+ const employees = await Employee.find()
6
+ .populate('department')
7
+ .populate('position')
8
+ .sort({ createdAt: -1 });
9
+
10
+ res.status(200).json({ success: true, count: employees.length, data: employees });
11
+ } catch (error) {
12
+ next(error);
13
+ }
14
+ };
15
+
16
+ export const getEmployee = async (req, res, next) => {
17
+ try {
18
+ const employee = await Employee.findById(req.params.id)
19
+ .populate('department')
20
+ .populate('position');
21
+
22
+ if (!employee) {
23
+ return res.status(404).json({ success: false, message: 'Employee not found' });
24
+ }
25
+
26
+ res.status(200).json({ success: true, data: employee });
27
+ } catch (error) {
28
+ next(error);
29
+ }
30
+ };
31
+
32
+ export const createEmployee = async (req, res, next) => {
33
+ try {
34
+ const employee = await Employee.create(req.body);
35
+ const populated = await Employee.findById(employee._id)
36
+ .populate('department')
37
+ .populate('position');
38
+
39
+ res.status(201).json({ success: true, data: populated });
40
+ } catch (error) {
41
+ next(error);
42
+ }
43
+ };
44
+
45
+ export const updateEmployee = async (req, res, next) => {
46
+ try {
47
+ const employee = await Employee.findByIdAndUpdate(req.params.id, req.body, {
48
+ new: true,
49
+ runValidators: true,
50
+ })
51
+ .populate('department')
52
+ .populate('position');
53
+
54
+ if (!employee) {
55
+ return res.status(404).json({ success: false, message: 'Employee not found' });
56
+ }
57
+
58
+ res.status(200).json({ success: true, data: employee });
59
+ } catch (error) {
60
+ next(error);
61
+ }
62
+ };
63
+
64
+ export const deleteEmployee = async (req, res, next) => {
65
+ try {
66
+ const employee = await Employee.findByIdAndDelete(req.params.id);
67
+
68
+ if (!employee) {
69
+ return res.status(404).json({ success: false, message: 'Employee not found' });
70
+ }
71
+
72
+ res.status(200).json({ success: true, message: 'Employee deleted successfully' });
73
+ } catch (error) {
74
+ next(error);
75
+ }
76
+ };
77
+
78
+ // 1. FIXED & UPDATED STATS METHOD
79
+ export const getEmployeeStats = async (req, res, next) => {
80
+ try {
81
+ // Run separate asynchronous database counts in parallel for optimal efficiency
82
+ const [
83
+ total,
84
+ active,
85
+ onLeave,
86
+ left,
87
+ blacklisted,
88
+ deceased,
89
+ onMission
90
+ ] = await Promise.all([
91
+ Employee.countDocuments(),
92
+ Employee.countDocuments({ empStatus: 'Active' }),
93
+ Employee.countDocuments({ empStatus: 'On Leave' }),
94
+ Employee.countDocuments({ empStatus: 'Left' }),
95
+ Employee.countDocuments({ empStatus: 'Blacklisted' }),
96
+ Employee.countDocuments({ empStatus: 'Deceased' }),
97
+ Employee.countDocuments({ empStatus: 'On Mission' })
98
+ ]);
99
+
100
+ const byDepartment = await Employee.aggregate([
101
+ {
102
+ $lookup: {
103
+ from: 'departments',
104
+ localField: 'department',
105
+ foreignField: '_id',
106
+ as: 'dept',
107
+ },
108
+ },
109
+ { $unwind: '$dept' },
110
+ { $group: { _id: '$dept.departName', count: { $sum: 1 } } },
111
+ { $sort: { count: -1 } },
112
+ ]);
113
+
114
+ res.status(200).json({
115
+ success: true,
116
+ data: {
117
+ total,
118
+ active,
119
+ onLeave,
120
+ left,
121
+ blacklisted,
122
+ deceased,
123
+ onMission,
124
+ byDepartment
125
+ },
126
+ });
127
+ } catch (error) {
128
+ next(error);
129
+ }
130
+ };
131
+
132
+ // Leave Report
133
+ export const getLeaveReport = async (req, res, next) => {
134
+ try {
135
+ const leaveReport = await Employee.aggregate([
136
+ // A. Match only people on leave
137
+ { $match: { empStatus: 'On Leave' } },
138
+ // B. Lookup relationship attributes inside the departments collection
139
+ {
140
+ $lookup: {
141
+ from: 'departments',
142
+ localField: 'department',
143
+ foreignField: '_id',
144
+ as: 'deptDetails'
145
+ }
146
+ },
147
+ { $unwind: { path: '$deptDetails', preserveNullAndEmptyArrays: true } },
148
+ // C. Group documents under department designations
149
+ {
150
+ $group: {
151
+ _id: { $ifNull: ['$deptDetails.departName', 'Unassigned Department'] },
152
+ employees: {
153
+ $push: {
154
+ id: '$_id',
155
+ name: { $concat: ['$empFirstName', ' ', '$empLastName'] },
156
+ email: '$empEmail',
157
+ telephone: '$empTelephone',
158
+ status: '$empStatus'
159
+ }
160
+ },
161
+ totalInDepartment: { $sum: 1 }
162
+ }
163
+ },
164
+ { $sort: { _id: 1 } }
165
+ ]);
166
+
167
+ // Calculate absolute sum aggregate metrics total
168
+ const grandTotal = leaveReport.reduce((acc, current) => acc + current.totalInDepartment, 0);
169
+
170
+ res.status(200).json({
171
+ success: true,
172
+ totalEmployeesOnLeave: grandTotal,
173
+ reportData: leaveReport
174
+ });
175
+ } catch (error) {
176
+ next(error);
177
+ }
178
+ };
@@ -0,0 +1,66 @@
1
+ import Position from '../models/Position.js';
2
+ import Employee from '../models/Employee.js';
3
+
4
+ export const getPositions = async (req, res, next) => {
5
+ try {
6
+ const positions = await Position.find().sort({ posName: 1 });
7
+ res.status(200).json({ success: true, count: positions.length, data: positions });
8
+ } catch (error) {
9
+ next(error);
10
+ }
11
+ };
12
+
13
+ export const getPosition = async (req, res, next) => {
14
+ try {
15
+ const position = await Position.findById(req.params.id);
16
+ if (!position) {
17
+ return res.status(404).json({ success: false, message: 'Position not found' });
18
+ }
19
+ res.status(200).json({ success: true, data: position });
20
+ } catch (error) {
21
+ next(error);
22
+ }
23
+ };
24
+
25
+ export const createPosition = async (req, res, next) => {
26
+ try {
27
+ const position = await Position.create(req.body);
28
+ res.status(201).json({ success: true, data: position });
29
+ } catch (error) {
30
+ next(error);
31
+ }
32
+ };
33
+
34
+ export const updatePosition = async (req, res, next) => {
35
+ try {
36
+ const position = await Position.findByIdAndUpdate(req.params.id, req.body, {
37
+ new: true,
38
+ runValidators: true,
39
+ });
40
+ if (!position) {
41
+ return res.status(404).json({ success: false, message: 'Position not found' });
42
+ }
43
+ res.status(200).json({ success: true, data: position });
44
+ } catch (error) {
45
+ next(error);
46
+ }
47
+ };
48
+
49
+ export const deletePosition = async (req, res, next) => {
50
+ try {
51
+ const count = await Employee.countDocuments({ position: req.params.id });
52
+ if (count > 0) {
53
+ return res.status(400).json({
54
+ success: false,
55
+ message: `Cannot delete position with ${count} assigned employee(s)`,
56
+ });
57
+ }
58
+ const position = await Position.findByIdAndDelete(req.params.id);
59
+ if (!position) {
60
+ return res.status(404).json({ success: false, message: 'Position not found' });
61
+ }
62
+ res.status(200).json({ success: true, message: 'Position deleted successfully' });
63
+ } catch (error) {
64
+ next(error);
65
+ }
66
+ };
@@ -0,0 +1,57 @@
1
+ import jwt from 'jsonwebtoken';
2
+ import User from '../models/User.js';
3
+
4
+ /**
5
+ * @desc Protect routes - Verify JWT token and attach fully populated user to request
6
+ */
7
+ export const protect = async (req, res, next) => {
8
+ let token;
9
+
10
+ // 1. Check for token in the Authorization header
11
+ if (
12
+ req.headers.authorization &&
13
+ req.headers.authorization.startsWith('Bearer')
14
+ ) {
15
+ token = req.headers.authorization.split(' ')[1];
16
+ }
17
+
18
+ // 2. Make sure token exists
19
+ if (!token) {
20
+ return res.status(401).json({
21
+ success: false,
22
+ message: 'Not authorized to access this route. Token missing.'
23
+ });
24
+ }
25
+
26
+ try {
27
+ // 3. Verify token encryption integrity
28
+ const decoded = jwt.verify(token, process.env.JWT_SECRET);
29
+
30
+ // 4. Fetch the user identity and deeply populate structural profile links
31
+ // This matches the exact payload configuration of the login controller
32
+ req.user = await User.findById(decoded.id).populate({
33
+ path: 'employee',
34
+ populate: [
35
+ { path: 'department' },
36
+ { path: 'position' }
37
+ ],
38
+ });
39
+
40
+ // 5. Ensure the database document still exists
41
+ if (!req.user) {
42
+ return res.status(401).json({
43
+ success: false,
44
+ message: 'The user belonging to this token no longer exists.'
45
+ });
46
+ }
47
+
48
+ // 6. Grant access to the protected controller route
49
+ next();
50
+ } catch (error) {
51
+ console.error('Auth middleware verification failure:', error.message);
52
+ return res.status(401).json({
53
+ success: false,
54
+ message: 'Not authorized to access this route. Session expired or invalid.'
55
+ });
56
+ }
57
+ };
@@ -0,0 +1,32 @@
1
+ export const notFound = (req, res, next) => {
2
+ const error = new Error(`Not found - ${req.originalUrl}`);
3
+ res.status(404);
4
+ next(error);
5
+ };
6
+
7
+ export const errorHandler = (err, req, res, next) => {
8
+ const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
9
+
10
+ if (err.name === 'ValidationError') {
11
+ const messages = Object.values(err.errors).map((e) => e.message);
12
+ return res.status(400).json({ success: false, message: messages.join(', ') });
13
+ }
14
+
15
+ if (err.code === 11000) {
16
+ const field = Object.keys(err.keyValue)[0];
17
+ return res.status(400).json({
18
+ success: false,
19
+ message: `${field} already exists.`,
20
+ });
21
+ }
22
+
23
+ if (err.name === 'CastError') {
24
+ return res.status(400).json({ success: false, message: 'Invalid resource ID.' });
25
+ }
26
+
27
+ res.status(statusCode).json({
28
+ success: false,
29
+ message: err.message || 'Server error',
30
+ ...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
31
+ });
32
+ };
@@ -0,0 +1,5 @@
1
+ // Neutralized middleware to guarantee system compatibility across legacy controller layers
2
+ export const restrictToAdmin = (req, res, next) => {
3
+ // Everyone has absolute privileges - immediately pass control down the chain
4
+ next();
5
+ };
@@ -0,0 +1,13 @@
1
+ import { validationResult } from 'express-validator';
2
+
3
+ export const validate = (req, res, next) => {
4
+ const errors = validationResult(req);
5
+ if (!errors.isEmpty()) {
6
+ return res.status(400).json({
7
+ success: false,
8
+ message: 'Validation failed',
9
+ errors: errors.array().map((e) => ({ field: e.path, message: e.msg })),
10
+ });
11
+ }
12
+ next();
13
+ };
@@ -0,0 +1,19 @@
1
+ import mongoose from 'mongoose';
2
+
3
+ const departmentSchema = new mongoose.Schema(
4
+ {
5
+ departName: {
6
+ type: String,
7
+ required: [true, 'Department name is required'],
8
+ unique: true,
9
+ trim: true,
10
+ },
11
+ description: {
12
+ type: String,
13
+ trim: true,
14
+ },
15
+ },
16
+ { timestamps: true }
17
+ );
18
+
19
+ export default mongoose.model('Department', departmentSchema);