mern-template-tool 1.0.0
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.
- package/README.md +93 -0
- package/bin/index.js +112 -0
- package/package.json +33 -0
- package/templates/README.md +160 -0
- package/templates/_gitignore +8 -0
- package/templates/client/index.html +13 -0
- package/templates/client/package.json +20 -0
- package/templates/client/src/App.jsx +72 -0
- package/templates/client/src/components/Loading.jsx +14 -0
- package/templates/client/src/components/Navbar.jsx +58 -0
- package/templates/client/src/context/AuthContext.jsx +109 -0
- package/templates/client/src/index.css +244 -0
- package/templates/client/src/main.jsx +16 -0
- package/templates/client/src/pages/CreateResource.jsx +142 -0
- package/templates/client/src/pages/Dashboard.jsx +117 -0
- package/templates/client/src/pages/EditResource.jsx +166 -0
- package/templates/client/src/pages/Login.jsx +101 -0
- package/templates/client/src/pages/NotFound.jsx +21 -0
- package/templates/client/src/pages/Profile.jsx +52 -0
- package/templates/client/src/pages/Register.jsx +134 -0
- package/templates/client/src/pages/ResourceDetails.jsx +156 -0
- package/templates/client/src/pages/Resources.jsx +181 -0
- package/templates/client/src/routes/ProtectedRoute.jsx +34 -0
- package/templates/client/src/services/api.js +71 -0
- package/templates/client/vite.config.js +20 -0
- package/templates/server/.env.example +5 -0
- package/templates/server/config/db.js +13 -0
- package/templates/server/controllers/authController.js +131 -0
- package/templates/server/controllers/resourceController.js +254 -0
- package/templates/server/middleware/adminMiddleware.js +14 -0
- package/templates/server/middleware/authMiddleware.js +53 -0
- package/templates/server/middleware/errorMiddleware.js +55 -0
- package/templates/server/middleware/sessionAuthMiddleware.js +12 -0
- package/templates/server/middleware/uploadMiddleware.js +61 -0
- package/templates/server/middleware/validateMiddleware.js +61 -0
- package/templates/server/models/Resource.js +43 -0
- package/templates/server/models/User.js +48 -0
- package/templates/server/package.json +25 -0
- package/templates/server/routes/adminRoutes.js +138 -0
- package/templates/server/routes/authRoutes.js +21 -0
- package/templates/server/routes/resourceRoutes.js +27 -0
- package/templates/server/server.js +89 -0
- package/templates/server/utils/jwt.js +18 -0
- package/templates/server/views/dashboard.ejs +128 -0
- package/templates/server/views/login.ejs +48 -0
- package/templates/server/views/partials/footer.ejs +6 -0
- package/templates/server/views/partials/header.ejs +60 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const Resource = require('../models/Resource');
|
|
4
|
+
|
|
5
|
+
// @desc Get all resources (with search, category filter, and sort)
|
|
6
|
+
// @route GET /api/resources
|
|
7
|
+
// @access Public
|
|
8
|
+
const getResources = async (req, res, next) => {
|
|
9
|
+
try {
|
|
10
|
+
const { search, category, sort } = req.query;
|
|
11
|
+
|
|
12
|
+
// Build filter query
|
|
13
|
+
const queryObj = {};
|
|
14
|
+
|
|
15
|
+
// Search by title or description
|
|
16
|
+
if (search) {
|
|
17
|
+
queryObj.$or = [
|
|
18
|
+
{ title: { $regex: search, $options: 'i' } },
|
|
19
|
+
{ description: { $regex: search, $options: 'i' } },
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Filter by category
|
|
24
|
+
if (category && category !== 'All') {
|
|
25
|
+
queryObj.category = { $regex: `^${category}$`, $options: 'i' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let query = Resource.find(queryObj).populate('createdBy', 'name email role');
|
|
29
|
+
|
|
30
|
+
// Sorting
|
|
31
|
+
if (sort === 'newest') {
|
|
32
|
+
query = query.sort({ createdAt: -1 });
|
|
33
|
+
} else if (sort === 'oldest') {
|
|
34
|
+
query = query.sort({ createdAt: 1 });
|
|
35
|
+
} else if (sort === 'title_asc') {
|
|
36
|
+
query = query.sort({ title: 1 });
|
|
37
|
+
} else if (sort === 'title_desc') {
|
|
38
|
+
query = query.sort({ title: -1 });
|
|
39
|
+
} else {
|
|
40
|
+
query = query.sort({ createdAt: -1 }); // Default to newest
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const resources = await query;
|
|
44
|
+
|
|
45
|
+
return res.status(200).json({
|
|
46
|
+
success: true,
|
|
47
|
+
count: resources.length,
|
|
48
|
+
data: resources,
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
next(error);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// @desc Get single resource by ID
|
|
56
|
+
// @route GET /api/resources/:id
|
|
57
|
+
// @access Public
|
|
58
|
+
const getResourceById = async (req, res, next) => {
|
|
59
|
+
try {
|
|
60
|
+
const resource = await Resource.findById(req.params.id).populate(
|
|
61
|
+
'createdBy',
|
|
62
|
+
'name email role'
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (!resource) {
|
|
66
|
+
return res.status(404).json({
|
|
67
|
+
success: false,
|
|
68
|
+
message: 'Resource not found',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return res.status(200).json({
|
|
73
|
+
success: true,
|
|
74
|
+
data: resource,
|
|
75
|
+
});
|
|
76
|
+
} catch (error) {
|
|
77
|
+
next(error);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// @desc Create a new resource
|
|
82
|
+
// @route POST /api/resources
|
|
83
|
+
// @access Private (JWT)
|
|
84
|
+
const createResource = async (req, res, next) => {
|
|
85
|
+
try {
|
|
86
|
+
const { title, description, category } = req.body;
|
|
87
|
+
|
|
88
|
+
const resourceData = {
|
|
89
|
+
title,
|
|
90
|
+
description: description || '',
|
|
91
|
+
category: category || 'General',
|
|
92
|
+
createdBy: req.user._id,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// If file was uploaded via Multer
|
|
96
|
+
if (req.file) {
|
|
97
|
+
resourceData.fileName = req.file.originalname;
|
|
98
|
+
resourceData.fileUrl = `/uploads/${req.file.filename}`;
|
|
99
|
+
resourceData.fileSize = req.file.size;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const resource = await Resource.create(resourceData);
|
|
103
|
+
await resource.populate('createdBy', 'name email role');
|
|
104
|
+
|
|
105
|
+
return res.status(201).json({
|
|
106
|
+
success: true,
|
|
107
|
+
message: 'Resource created successfully',
|
|
108
|
+
data: resource,
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
next(error);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// @desc Update a resource
|
|
116
|
+
// @route PUT /api/resources/:id
|
|
117
|
+
// @access Private (Owner or Admin)
|
|
118
|
+
const updateResource = async (req, res, next) => {
|
|
119
|
+
try {
|
|
120
|
+
let resource = await Resource.findById(req.params.id);
|
|
121
|
+
|
|
122
|
+
if (!resource) {
|
|
123
|
+
return res.status(404).json({
|
|
124
|
+
success: false,
|
|
125
|
+
message: 'Resource not found',
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Ownership check: creator or admin
|
|
130
|
+
const isOwner = resource.createdBy.toString() === req.user._id.toString();
|
|
131
|
+
const isAdmin = req.user.role === 'admin';
|
|
132
|
+
|
|
133
|
+
if (!isOwner && !isAdmin) {
|
|
134
|
+
return res.status(403).json({
|
|
135
|
+
success: false,
|
|
136
|
+
message: 'Access denied: You can only modify resources that you created.',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const { title, description, category } = req.body;
|
|
141
|
+
|
|
142
|
+
if (title !== undefined) resource.title = title;
|
|
143
|
+
if (description !== undefined) resource.description = description;
|
|
144
|
+
if (category !== undefined) resource.category = category;
|
|
145
|
+
|
|
146
|
+
// Handle new file upload if present
|
|
147
|
+
if (req.file) {
|
|
148
|
+
// Delete old file if exists
|
|
149
|
+
if (resource.fileUrl) {
|
|
150
|
+
const oldFilePath = path.join(__dirname, '..', resource.fileUrl);
|
|
151
|
+
if (fs.existsSync(oldFilePath)) {
|
|
152
|
+
fs.unlinkSync(oldFilePath);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
resource.fileName = req.file.originalname;
|
|
156
|
+
resource.fileUrl = `/uploads/${req.file.filename}`;
|
|
157
|
+
resource.fileSize = req.file.size;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
await resource.save();
|
|
161
|
+
await resource.populate('createdBy', 'name email role');
|
|
162
|
+
|
|
163
|
+
return res.status(200).json({
|
|
164
|
+
success: true,
|
|
165
|
+
message: 'Resource updated successfully',
|
|
166
|
+
data: resource,
|
|
167
|
+
});
|
|
168
|
+
} catch (error) {
|
|
169
|
+
next(error);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// @desc Delete a resource
|
|
174
|
+
// @route DELETE /api/resources/:id
|
|
175
|
+
// @access Private (Owner or Admin)
|
|
176
|
+
const deleteResource = async (req, res, next) => {
|
|
177
|
+
try {
|
|
178
|
+
const resource = await Resource.findById(req.params.id);
|
|
179
|
+
|
|
180
|
+
if (!resource) {
|
|
181
|
+
return res.status(404).json({
|
|
182
|
+
success: false,
|
|
183
|
+
message: 'Resource not found',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Ownership check: creator or admin
|
|
188
|
+
const isOwner = resource.createdBy.toString() === req.user._id.toString();
|
|
189
|
+
const isAdmin = req.user.role === 'admin';
|
|
190
|
+
|
|
191
|
+
if (!isOwner && !isAdmin) {
|
|
192
|
+
return res.status(403).json({
|
|
193
|
+
success: false,
|
|
194
|
+
message: 'Access denied: You can only delete resources that you created.',
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Remove associated file from disk if present
|
|
199
|
+
if (resource.fileUrl) {
|
|
200
|
+
const filePath = path.join(__dirname, '..', resource.fileUrl);
|
|
201
|
+
if (fs.existsSync(filePath)) {
|
|
202
|
+
fs.unlinkSync(filePath);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await Resource.findByIdAndDelete(req.params.id);
|
|
207
|
+
|
|
208
|
+
return res.status(200).json({
|
|
209
|
+
success: true,
|
|
210
|
+
message: 'Resource deleted successfully',
|
|
211
|
+
data: { id: req.params.id },
|
|
212
|
+
});
|
|
213
|
+
} catch (error) {
|
|
214
|
+
next(error);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// @desc Download resource file
|
|
219
|
+
// @route GET /api/resources/:id/download
|
|
220
|
+
// @access Public
|
|
221
|
+
const downloadResourceFile = async (req, res, next) => {
|
|
222
|
+
try {
|
|
223
|
+
const resource = await Resource.findById(req.params.id);
|
|
224
|
+
|
|
225
|
+
if (!resource || !resource.fileUrl) {
|
|
226
|
+
return res.status(404).json({
|
|
227
|
+
success: false,
|
|
228
|
+
message: 'No file associated with this resource',
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const filePath = path.join(__dirname, '..', resource.fileUrl);
|
|
233
|
+
|
|
234
|
+
if (!fs.existsSync(filePath)) {
|
|
235
|
+
return res.status(404).json({
|
|
236
|
+
success: false,
|
|
237
|
+
message: 'File not found on server',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return res.download(filePath, resource.fileName || 'download');
|
|
242
|
+
} catch (error) {
|
|
243
|
+
next(error);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
module.exports = {
|
|
248
|
+
getResources,
|
|
249
|
+
getResourceById,
|
|
250
|
+
createResource,
|
|
251
|
+
updateResource,
|
|
252
|
+
deleteResource,
|
|
253
|
+
downloadResourceFile,
|
|
254
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const adminOnly = (req, res, next) => {
|
|
2
|
+
if (req.user && req.user.role === 'admin') {
|
|
3
|
+
next();
|
|
4
|
+
} else {
|
|
5
|
+
return res.status(403).json({
|
|
6
|
+
success: false,
|
|
7
|
+
message: 'Access denied: Admin role required for this action.',
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
adminOnly,
|
|
14
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const jwt = require('jsonwebtoken');
|
|
2
|
+
const User = require('../models/User');
|
|
3
|
+
|
|
4
|
+
const protect = async (req, res, next) => {
|
|
5
|
+
let token;
|
|
6
|
+
|
|
7
|
+
if (
|
|
8
|
+
req.headers.authorization &&
|
|
9
|
+
req.headers.authorization.startsWith('Bearer')
|
|
10
|
+
) {
|
|
11
|
+
try {
|
|
12
|
+
token = req.headers.authorization.split(' ')[1];
|
|
13
|
+
|
|
14
|
+
const decoded = jwt.verify(
|
|
15
|
+
token,
|
|
16
|
+
process.env.JWT_SECRET || 'fallback_jwt_secret_for_exam'
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const user = await User.findById(decoded.id).select('-password');
|
|
20
|
+
|
|
21
|
+
if (!user) {
|
|
22
|
+
return res.status(401).json({
|
|
23
|
+
success: false,
|
|
24
|
+
message: 'User belonging to this token no longer exists.',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!user.isActive) {
|
|
29
|
+
return res.status(403).json({
|
|
30
|
+
success: false,
|
|
31
|
+
message: 'User account is deactivated.',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
req.user = user;
|
|
36
|
+
next();
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return res.status(401).json({
|
|
39
|
+
success: false,
|
|
40
|
+
message: 'Not authorized, token verification failed.',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
return res.status(401).json({
|
|
45
|
+
success: false,
|
|
46
|
+
message: 'Not authorized, no Bearer token provided in Authorization header.',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
protect,
|
|
53
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// 404 Handler
|
|
2
|
+
const notFound = (req, res, next) => {
|
|
3
|
+
const error = new Error(`Not Found - ${req.originalUrl}`);
|
|
4
|
+
res.status(404);
|
|
5
|
+
next(error);
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// Centralized Error Handler
|
|
9
|
+
const errorHandler = (err, req, res, next) => {
|
|
10
|
+
let statusCode = res.statusCode === 200 ? 500 : res.statusCode;
|
|
11
|
+
|
|
12
|
+
// Handle Mongoose Bad ObjectId (CastError)
|
|
13
|
+
if (err.name === 'CastError' && err.kind === 'ObjectId') {
|
|
14
|
+
statusCode = 404;
|
|
15
|
+
err.message = 'Resource not found (invalid ID format)';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Handle Mongoose Duplicate Key Error
|
|
19
|
+
if (err.code === 11000) {
|
|
20
|
+
statusCode = 400;
|
|
21
|
+
const field = Object.keys(err.keyValue)[0];
|
|
22
|
+
err.message = `Duplicate value entered for ${field} field.`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Handle Multer errors
|
|
26
|
+
if (err.name === 'MulterError') {
|
|
27
|
+
statusCode = 400;
|
|
28
|
+
if (err.code === 'LIMIT_FILE_SIZE') {
|
|
29
|
+
err.message = 'File size limit exceeded (Max 10MB).';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// API Requests get JSON response
|
|
34
|
+
if (req.originalUrl.startsWith('/api')) {
|
|
35
|
+
return res.status(statusCode).json({
|
|
36
|
+
success: false,
|
|
37
|
+
message: err.message || 'Internal Server Error',
|
|
38
|
+
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Render error for browser/EJS admin routes
|
|
43
|
+
res.status(statusCode).send(`
|
|
44
|
+
<div style="font-family: sans-serif; padding: 2rem; text-align: center;">
|
|
45
|
+
<h2>Error ${statusCode}</h2>
|
|
46
|
+
<p>${err.message || 'Something went wrong.'}</p>
|
|
47
|
+
<a href="/admin/login">Return to Admin Login</a>
|
|
48
|
+
</div>
|
|
49
|
+
`);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
notFound,
|
|
54
|
+
errorHandler,
|
|
55
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const sessionAuth = (req, res, next) => {
|
|
2
|
+
if (req.session && req.session.user && req.session.user.role === 'admin') {
|
|
3
|
+
res.locals.adminUser = req.session.user;
|
|
4
|
+
next();
|
|
5
|
+
} else {
|
|
6
|
+
return res.redirect('/admin/login?error=Please%20login%20as%20an%20admin%20to%20access%20dashboard');
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
sessionAuth,
|
|
12
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const multer = require('multer');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
// Ensure uploads folder exists
|
|
6
|
+
const uploadDir = path.join(__dirname, '..', 'uploads');
|
|
7
|
+
if (!fs.existsSync(uploadDir)) {
|
|
8
|
+
fs.mkdirSync(uploadDir, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Storage Configuration
|
|
12
|
+
const storage = multer.diskStorage({
|
|
13
|
+
destination: function (req, file, cb) {
|
|
14
|
+
cb(null, uploadDir);
|
|
15
|
+
},
|
|
16
|
+
filename: function (req, file, cb) {
|
|
17
|
+
// Generate safe, unique filename: timestamp-random-originalname
|
|
18
|
+
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
|
19
|
+
const sanitizedExt = path.extname(file.originalname).toLowerCase();
|
|
20
|
+
const sanitizedBase = path
|
|
21
|
+
.basename(file.originalname, sanitizedExt)
|
|
22
|
+
.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
23
|
+
cb(null, `${sanitizedBase}-${uniqueSuffix}${sanitizedExt}`);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// File filter for supported file types: pdf, png, jpg, jpeg, doc, docx
|
|
28
|
+
const fileFilter = (req, file, cb) => {
|
|
29
|
+
const allowedExtensions = /pdf|png|jpg|jpeg|doc|docx/;
|
|
30
|
+
const extname = allowedExtensions.test(
|
|
31
|
+
path.extname(file.originalname).toLowerCase()
|
|
32
|
+
);
|
|
33
|
+
const mimetype =
|
|
34
|
+
allowedExtensions.test(file.mimetype) ||
|
|
35
|
+
file.mimetype === 'application/pdf' ||
|
|
36
|
+
file.mimetype === 'image/png' ||
|
|
37
|
+
file.mimetype === 'image/jpeg' ||
|
|
38
|
+
file.mimetype === 'application/msword' ||
|
|
39
|
+
file.mimetype ===
|
|
40
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
|
41
|
+
|
|
42
|
+
if (extname || mimetype) {
|
|
43
|
+
return cb(null, true);
|
|
44
|
+
} else {
|
|
45
|
+
return cb(
|
|
46
|
+
new Error(
|
|
47
|
+
'Invalid file type. Only PDF, PNG, JPG, JPEG, DOC, and DOCX files are allowed.'
|
|
48
|
+
),
|
|
49
|
+
false
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// 10MB limit
|
|
55
|
+
const upload = multer({
|
|
56
|
+
storage: storage,
|
|
57
|
+
limits: { fileSize: 10 * 1024 * 1024 },
|
|
58
|
+
fileFilter: fileFilter,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
module.exports = upload;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const { body, validationResult } = require('express-validator');
|
|
2
|
+
|
|
3
|
+
// Reusable middleware to handle validation results
|
|
4
|
+
const validate = (req, res, next) => {
|
|
5
|
+
const errors = validationResult(req);
|
|
6
|
+
if (!errors.isEmpty()) {
|
|
7
|
+
return res.status(400).json({
|
|
8
|
+
success: false,
|
|
9
|
+
message: 'Validation failed',
|
|
10
|
+
errors: errors.array().map((err) => ({
|
|
11
|
+
field: err.path || err.param,
|
|
12
|
+
message: err.msg,
|
|
13
|
+
})),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
next();
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// User Registration Validation Rules
|
|
20
|
+
const registerValidationRules = [
|
|
21
|
+
body('name').trim().notEmpty().withMessage('Name is required'),
|
|
22
|
+
body('email')
|
|
23
|
+
.trim()
|
|
24
|
+
.isEmail()
|
|
25
|
+
.withMessage('Please provide a valid email address')
|
|
26
|
+
.normalizeEmail(),
|
|
27
|
+
body('password')
|
|
28
|
+
.isLength({ min: 6 })
|
|
29
|
+
.withMessage('Password must be at least 6 characters long'),
|
|
30
|
+
body('role')
|
|
31
|
+
.optional()
|
|
32
|
+
.isIn(['student', 'admin'])
|
|
33
|
+
.withMessage('Role must be either student or admin'),
|
|
34
|
+
validate,
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// User Login Validation Rules
|
|
38
|
+
const loginValidationRules = [
|
|
39
|
+
body('email')
|
|
40
|
+
.trim()
|
|
41
|
+
.isEmail()
|
|
42
|
+
.withMessage('Please provide a valid email address')
|
|
43
|
+
.normalizeEmail(),
|
|
44
|
+
body('password').notEmpty().withMessage('Password is required'),
|
|
45
|
+
validate,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// Generic Resource Validation Rules
|
|
49
|
+
const resourceValidationRules = [
|
|
50
|
+
body('title').trim().notEmpty().withMessage('Title is required'),
|
|
51
|
+
body('description').optional().trim(),
|
|
52
|
+
body('category').optional().trim(),
|
|
53
|
+
validate,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
module.exports = {
|
|
57
|
+
validate,
|
|
58
|
+
registerValidationRules,
|
|
59
|
+
loginValidationRules,
|
|
60
|
+
resourceValidationRules,
|
|
61
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const resourceSchema = new mongoose.Schema(
|
|
4
|
+
{
|
|
5
|
+
title: {
|
|
6
|
+
type: String,
|
|
7
|
+
required: [true, 'Please provide a title'],
|
|
8
|
+
trim: true,
|
|
9
|
+
},
|
|
10
|
+
description: {
|
|
11
|
+
type: String,
|
|
12
|
+
trim: true,
|
|
13
|
+
default: '',
|
|
14
|
+
},
|
|
15
|
+
category: {
|
|
16
|
+
type: String,
|
|
17
|
+
trim: true,
|
|
18
|
+
default: 'General',
|
|
19
|
+
},
|
|
20
|
+
fileUrl: {
|
|
21
|
+
type: String,
|
|
22
|
+
default: null,
|
|
23
|
+
},
|
|
24
|
+
fileName: {
|
|
25
|
+
type: String,
|
|
26
|
+
default: null,
|
|
27
|
+
},
|
|
28
|
+
fileSize: {
|
|
29
|
+
type: Number,
|
|
30
|
+
default: null,
|
|
31
|
+
},
|
|
32
|
+
createdBy: {
|
|
33
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
34
|
+
ref: 'User',
|
|
35
|
+
required: true,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
timestamps: true,
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
module.exports = mongoose.model('Resource', resourceSchema);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const userSchema = new mongoose.Schema(
|
|
4
|
+
{
|
|
5
|
+
name: {
|
|
6
|
+
type: String,
|
|
7
|
+
required: [true, 'Please provide a name'],
|
|
8
|
+
trim: true,
|
|
9
|
+
},
|
|
10
|
+
email: {
|
|
11
|
+
type: String,
|
|
12
|
+
required: [true, 'Please provide an email'],
|
|
13
|
+
unique: true,
|
|
14
|
+
lowercase: true,
|
|
15
|
+
trim: true,
|
|
16
|
+
match: [
|
|
17
|
+
/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/,
|
|
18
|
+
'Please provide a valid email address',
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
password: {
|
|
22
|
+
type: String,
|
|
23
|
+
required: [true, 'Please provide a password'],
|
|
24
|
+
minlength: 6,
|
|
25
|
+
},
|
|
26
|
+
role: {
|
|
27
|
+
type: String,
|
|
28
|
+
enum: ['student', 'admin'],
|
|
29
|
+
default: 'student',
|
|
30
|
+
},
|
|
31
|
+
isActive: {
|
|
32
|
+
type: Boolean,
|
|
33
|
+
default: true,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
timestamps: true,
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Method to remove password from user object when returning JSON
|
|
42
|
+
userSchema.methods.toJSON = function () {
|
|
43
|
+
const userObj = this.toObject();
|
|
44
|
+
delete userObj.password;
|
|
45
|
+
return userObj;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
module.exports = mongoose.model('User', userSchema);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Exam-ready MERN backend with Express, MongoDB, JWT, Sessions, EJS, and Multer",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node server.js",
|
|
8
|
+
"dev": "nodemon server.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"bcryptjs": "^2.4.3",
|
|
12
|
+
"cors": "^2.8.5",
|
|
13
|
+
"dotenv": "^16.4.5",
|
|
14
|
+
"ejs": "^3.1.10",
|
|
15
|
+
"express": "^4.19.2",
|
|
16
|
+
"express-session": "^1.18.0",
|
|
17
|
+
"express-validator": "^7.1.0",
|
|
18
|
+
"jsonwebtoken": "^9.0.2",
|
|
19
|
+
"mongoose": "^8.4.1",
|
|
20
|
+
"multer": "^1.4.5-lts.1"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"nodemon": "^3.1.2"
|
|
24
|
+
}
|
|
25
|
+
}
|