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.
Files changed (47) hide show
  1. package/README.md +93 -0
  2. package/bin/index.js +112 -0
  3. package/package.json +33 -0
  4. package/templates/README.md +160 -0
  5. package/templates/_gitignore +8 -0
  6. package/templates/client/index.html +13 -0
  7. package/templates/client/package.json +20 -0
  8. package/templates/client/src/App.jsx +72 -0
  9. package/templates/client/src/components/Loading.jsx +14 -0
  10. package/templates/client/src/components/Navbar.jsx +58 -0
  11. package/templates/client/src/context/AuthContext.jsx +109 -0
  12. package/templates/client/src/index.css +244 -0
  13. package/templates/client/src/main.jsx +16 -0
  14. package/templates/client/src/pages/CreateResource.jsx +142 -0
  15. package/templates/client/src/pages/Dashboard.jsx +117 -0
  16. package/templates/client/src/pages/EditResource.jsx +166 -0
  17. package/templates/client/src/pages/Login.jsx +101 -0
  18. package/templates/client/src/pages/NotFound.jsx +21 -0
  19. package/templates/client/src/pages/Profile.jsx +52 -0
  20. package/templates/client/src/pages/Register.jsx +134 -0
  21. package/templates/client/src/pages/ResourceDetails.jsx +156 -0
  22. package/templates/client/src/pages/Resources.jsx +181 -0
  23. package/templates/client/src/routes/ProtectedRoute.jsx +34 -0
  24. package/templates/client/src/services/api.js +71 -0
  25. package/templates/client/vite.config.js +20 -0
  26. package/templates/server/.env.example +5 -0
  27. package/templates/server/config/db.js +13 -0
  28. package/templates/server/controllers/authController.js +131 -0
  29. package/templates/server/controllers/resourceController.js +254 -0
  30. package/templates/server/middleware/adminMiddleware.js +14 -0
  31. package/templates/server/middleware/authMiddleware.js +53 -0
  32. package/templates/server/middleware/errorMiddleware.js +55 -0
  33. package/templates/server/middleware/sessionAuthMiddleware.js +12 -0
  34. package/templates/server/middleware/uploadMiddleware.js +61 -0
  35. package/templates/server/middleware/validateMiddleware.js +61 -0
  36. package/templates/server/models/Resource.js +43 -0
  37. package/templates/server/models/User.js +48 -0
  38. package/templates/server/package.json +25 -0
  39. package/templates/server/routes/adminRoutes.js +138 -0
  40. package/templates/server/routes/authRoutes.js +21 -0
  41. package/templates/server/routes/resourceRoutes.js +27 -0
  42. package/templates/server/server.js +89 -0
  43. package/templates/server/utils/jwt.js +18 -0
  44. package/templates/server/views/dashboard.ejs +128 -0
  45. package/templates/server/views/login.ejs +48 -0
  46. package/templates/server/views/partials/footer.ejs +6 -0
  47. 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
+ }