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,181 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import { api } from '../services/api';
4
+ import Loading from '../components/Loading';
5
+
6
+ const Resources = () => {
7
+ const [resources, setResources] = useState([]);
8
+ const [loading, setLoading] = useState(true);
9
+ const [search, setSearch] = useState('');
10
+ const [category, setCategory] = useState('All');
11
+ const [sort, setSort] = useState('newest');
12
+
13
+ const fetchResources = async () => {
14
+ setLoading(true);
15
+ try {
16
+ const response = await api.getResources({
17
+ search,
18
+ category,
19
+ sort,
20
+ });
21
+ if (response.success) {
22
+ setResources(response.data);
23
+ }
24
+ } catch (err) {
25
+ console.error('Failed to fetch resources:', err);
26
+ } finally {
27
+ setLoading(false);
28
+ }
29
+ };
30
+
31
+ useEffect(() => {
32
+ fetchResources();
33
+ }, [category, sort]);
34
+
35
+ const handleSearchSubmit = (e) => {
36
+ e.preventDefault();
37
+ fetchResources();
38
+ };
39
+
40
+ return (
41
+ <div className="container">
42
+ <div
43
+ style={{
44
+ display: 'flex',
45
+ justifyContent: 'space-between',
46
+ alignItems: 'center',
47
+ marginBottom: '1.5rem',
48
+ }}
49
+ >
50
+ <div>
51
+ <h2>All Resources</h2>
52
+ <p>Explore, search, filter, and download uploaded items.</p>
53
+ </div>
54
+ <Link to="/resources/new" className="btn btn-primary">
55
+ + Add New Resource
56
+ </Link>
57
+ </div>
58
+
59
+ {/* Filter and Search Bar */}
60
+ <div className="card" style={{ padding: '1rem' }}>
61
+ <form
62
+ onSubmit={handleSearchSubmit}
63
+ style={{
64
+ display: 'grid',
65
+ gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
66
+ gap: '1rem',
67
+ alignItems: 'center',
68
+ }}
69
+ >
70
+ {/* Search Input */}
71
+ <div className="form-group" style={{ marginBottom: 0 }}>
72
+ <input
73
+ type="text"
74
+ className="form-control"
75
+ placeholder="Search by title or description..."
76
+ value={search}
77
+ onChange={(e) => setSearch(e.target.value)}
78
+ />
79
+ </div>
80
+
81
+ {/* Category Filter */}
82
+ <div className="form-group" style={{ marginBottom: 0 }}>
83
+ <select
84
+ className="form-control"
85
+ value={category}
86
+ onChange={(e) => setCategory(e.target.value)}
87
+ >
88
+ <option value="All">All Categories</option>
89
+ <option value="General">General</option>
90
+ <option value="Documentation">Documentation</option>
91
+ <option value="Assignment">Assignment</option>
92
+ <option value="Project">Project</option>
93
+ <option value="Exam">Exam</option>
94
+ </select>
95
+ </div>
96
+
97
+ {/* Sort Option */}
98
+ <div className="form-group" style={{ marginBottom: 0 }}>
99
+ <select
100
+ className="form-control"
101
+ value={sort}
102
+ onChange={(e) => setSort(e.target.value)}
103
+ >
104
+ <option value="newest">Sort by: Newest</option>
105
+ <option value="oldest">Sort by: Oldest</option>
106
+ <option value="title_asc">Title (A-Z)</option>
107
+ <option value="title_desc">Title (Z-A)</option>
108
+ </select>
109
+ </div>
110
+
111
+ {/* Search Button */}
112
+ <button type="submit" className="btn btn-secondary">
113
+ Search
114
+ </button>
115
+ </form>
116
+ </div>
117
+
118
+ {/* Resource List */}
119
+ {loading ? (
120
+ <Loading message="Fetching resources..." />
121
+ ) : resources.length === 0 ? (
122
+ <div className="card" style={{ textAlign: 'center', padding: '3rem' }}>
123
+ <h3>No resources found</h3>
124
+ <p style={{ marginTop: '0.5rem' }}>
125
+ Try adjusting your search query or filter criteria.
126
+ </p>
127
+ </div>
128
+ ) : (
129
+ <div className="grid-cols-3">
130
+ {resources.map((item) => (
131
+ <div key={item._id} className="card" style={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
132
+ <div>
133
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '0.5rem' }}>
134
+ <span className="badge badge-primary">{item.category}</span>
135
+ <span style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>
136
+ {new Date(item.createdAt).toLocaleDateString()}
137
+ </span>
138
+ </div>
139
+ <h3 style={{ fontSize: '1.2rem', marginBottom: '0.5rem' }}>{item.title}</h3>
140
+ <p style={{ fontSize: '0.9rem', marginBottom: '1rem' }}>
141
+ {item.description ? (
142
+ item.description.length > 90
143
+ ? `${item.description.substring(0, 90)}...`
144
+ : item.description
145
+ ) : (
146
+ <em style={{ color: 'var(--text-secondary)' }}>No description provided.</em>
147
+ )}
148
+ </p>
149
+ </div>
150
+
151
+ <div>
152
+ {item.fileUrl && (
153
+ <div style={{ marginBottom: '0.75rem', fontSize: '0.85rem' }}>
154
+ <a
155
+ href={`http://localhost:5000/api/resources/${item._id}/download`}
156
+ target="_blank"
157
+ rel="noreferrer"
158
+ style={{ color: '#38bdf8', textDecoration: 'none' }}
159
+ >
160
+ 📎 {item.fileName || 'Download Attachment'}
161
+ </a>
162
+ </div>
163
+ )}
164
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderTop: '1px solid var(--card-border)', paddingTop: '0.75rem' }}>
165
+ <span style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
166
+ By: {item.createdBy?.name || 'Anonymous'}
167
+ </span>
168
+ <Link to={`/resources/${item._id}`} className="btn btn-secondary btn-sm">
169
+ Details &rarr;
170
+ </Link>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ ))}
175
+ </div>
176
+ )}
177
+ </div>
178
+ );
179
+ };
180
+
181
+ export default Resources;
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { Navigate, useLocation } from 'react-router-dom';
3
+ import { useAuth } from '../context/AuthContext';
4
+ import Loading from '../components/Loading';
5
+
6
+ const ProtectedRoute = ({ children, requiredRole = null }) => {
7
+ const { isAuthenticated, loading, user } = useAuth();
8
+ const location = useLocation();
9
+
10
+ if (loading) {
11
+ return <Loading message="Verifying session..." />;
12
+ }
13
+
14
+ if (!isAuthenticated) {
15
+ // Redirect to login preserving destination state
16
+ return <Navigate to="/login" state={{ from: location }} replace />;
17
+ }
18
+
19
+ // Role check if required
20
+ if (requiredRole && user?.role !== requiredRole) {
21
+ return (
22
+ <div className="container" style={{ textAlign: 'center', marginTop: '3rem' }}>
23
+ <div className="card">
24
+ <h2>Access Forbidden</h2>
25
+ <p>You do not have the required role ({requiredRole}) to view this page.</p>
26
+ </div>
27
+ </div>
28
+ );
29
+ }
30
+
31
+ return children;
32
+ };
33
+
34
+ export default ProtectedRoute;
@@ -0,0 +1,71 @@
1
+ const API_BASE_URL = '/api';
2
+
3
+ /**
4
+ * Reusable HTTP helper using native fetch API
5
+ * Automatically includes Authorization Bearer token from localStorage
6
+ */
7
+ async function request(endpoint, options = {}) {
8
+ const token = localStorage.getItem('token');
9
+ const headers = {
10
+ ...options.headers,
11
+ };
12
+
13
+ // Add auth header if token exists and not explicitly overridden
14
+ if (token) {
15
+ headers['Authorization'] = `Bearer ${token}`;
16
+ }
17
+
18
+ // If body is NOT FormData, set JSON Content-Type
19
+ if (options.body && !(options.body instanceof FormData)) {
20
+ headers['Content-Type'] = 'application/json';
21
+ options.body = JSON.stringify(options.body);
22
+ }
23
+
24
+ const response = await fetch(`${API_BASE_URL}${endpoint}`, {
25
+ ...options,
26
+ headers,
27
+ });
28
+
29
+ const data = await response.json().catch(() => ({}));
30
+
31
+ if (!response.ok) {
32
+ const error = new Error(data.message || 'An error occurred with the request');
33
+ error.status = response.status;
34
+ error.data = data;
35
+ throw error;
36
+ }
37
+
38
+ return data;
39
+ }
40
+
41
+ export const api = {
42
+ // Auth API
43
+ login: (credentials) =>
44
+ request('/auth/login', { method: 'POST', body: credentials }),
45
+ register: (userData) =>
46
+ request('/auth/register', { method: 'POST', body: userData }),
47
+ getMe: () => request('/auth/me', { method: 'GET' }),
48
+
49
+ // Generic Resources API
50
+ getResources: (params = {}) => {
51
+ const searchParams = new URLSearchParams();
52
+ if (params.search) searchParams.append('search', params.search);
53
+ if (params.category && params.category !== 'All')
54
+ searchParams.append('category', params.category);
55
+ if (params.sort) searchParams.append('sort', params.sort);
56
+
57
+ const queryString = searchParams.toString();
58
+ return request(`/resources${queryString ? `?${queryString}` : ''}`, {
59
+ method: 'GET',
60
+ });
61
+ },
62
+ getResourceById: (id) => request(`/resources/${id}`, { method: 'GET' }),
63
+ createResource: (formData) =>
64
+ request('/resources', { method: 'POST', body: formData }),
65
+ updateResource: (id, formData) =>
66
+ request(`/resources/${id}`, { method: 'PUT', body: formData }),
67
+ deleteResource: (id) =>
68
+ request(`/resources/${id}`, { method: 'DELETE' }),
69
+ };
70
+
71
+ export default api;
@@ -0,0 +1,20 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ // https://vitejs.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ server: {
8
+ port: 5173,
9
+ proxy: {
10
+ '/api': {
11
+ target: 'http://localhost:5000',
12
+ changeOrigin: true,
13
+ },
14
+ '/uploads': {
15
+ target: 'http://localhost:5000',
16
+ changeOrigin: true,
17
+ },
18
+ },
19
+ },
20
+ });
@@ -0,0 +1,5 @@
1
+ PORT=5000
2
+ MONGO_URI=mongodb://127.0.0.1:27017/mern_exam
3
+ JWT_SECRET=supersecretjwtkey_exam12345
4
+ SESSION_SECRET=supersecretsessionkey_exam12345
5
+ NODE_ENV=development
@@ -0,0 +1,13 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const connectDB = async () => {
4
+ try {
5
+ const conn = await mongoose.connect(process.env.MONGO_URI || 'mongodb://127.0.0.1:27017/mern_exam');
6
+ console.log(`[MongoDB] Connected to database: ${conn.connection.name} @ ${conn.connection.host}`);
7
+ } catch (error) {
8
+ console.error(`[MongoDB Error] Database connection failed: ${error.message}`);
9
+ process.exit(1);
10
+ }
11
+ };
12
+
13
+ module.exports = connectDB;
@@ -0,0 +1,131 @@
1
+ const bcrypt = require('bcryptjs');
2
+ const User = require('../models/User');
3
+ const { generateToken } = require('../utils/jwt');
4
+
5
+ // @desc Register a new user
6
+ // @route POST /api/auth/register
7
+ // @access Public
8
+ const registerUser = async (req, res, next) => {
9
+ try {
10
+ const { name, email, password, role } = req.body;
11
+
12
+ // Check if user already exists
13
+ const userExists = await User.findOne({ email: email.toLowerCase() });
14
+ if (userExists) {
15
+ return res.status(400).json({
16
+ success: false,
17
+ message: 'A user with this email already exists.',
18
+ });
19
+ }
20
+
21
+ // Hash password
22
+ const salt = await bcrypt.genSalt(10);
23
+ const hashedPassword = await bcrypt.hash(password, salt);
24
+
25
+ // Create user
26
+ const user = await User.create({
27
+ name,
28
+ email: email.toLowerCase(),
29
+ password: hashedPassword,
30
+ role: role || 'student',
31
+ });
32
+
33
+ // Generate JWT
34
+ const token = generateToken(user);
35
+
36
+ return res.status(201).json({
37
+ success: true,
38
+ message: 'User registered successfully.',
39
+ token,
40
+ user: {
41
+ id: user._id,
42
+ name: user.name,
43
+ email: user.email,
44
+ role: user.role,
45
+ isActive: user.isActive,
46
+ },
47
+ });
48
+ } catch (error) {
49
+ next(error);
50
+ }
51
+ };
52
+
53
+ // @desc Authenticate user & get token
54
+ // @route POST /api/auth/login
55
+ // @access Public
56
+ const loginUser = async (req, res, next) => {
57
+ try {
58
+ const { email, password } = req.body;
59
+
60
+ // Check for user
61
+ const user = await User.findOne({ email: email.toLowerCase() });
62
+ if (!user) {
63
+ return res.status(401).json({
64
+ success: false,
65
+ message: 'Invalid email or password.',
66
+ });
67
+ }
68
+
69
+ // Check if active
70
+ if (!user.isActive) {
71
+ return res.status(403).json({
72
+ success: false,
73
+ message: 'Your account has been deactivated. Please contact an admin.',
74
+ });
75
+ }
76
+
77
+ // Compare password
78
+ const isMatch = await bcrypt.compare(password, user.password);
79
+ if (!isMatch) {
80
+ return res.status(401).json({
81
+ success: false,
82
+ message: 'Invalid email or password.',
83
+ });
84
+ }
85
+
86
+ // Generate token
87
+ const token = generateToken(user);
88
+
89
+ return res.status(200).json({
90
+ success: true,
91
+ message: 'Login successful.',
92
+ token,
93
+ user: {
94
+ id: user._id,
95
+ name: user.name,
96
+ email: user.email,
97
+ role: user.role,
98
+ isActive: user.isActive,
99
+ },
100
+ });
101
+ } catch (error) {
102
+ next(error);
103
+ }
104
+ };
105
+
106
+ // @desc Get current authenticated user profile
107
+ // @route GET /api/auth/me
108
+ // @access Private (JWT)
109
+ const getMe = async (req, res, next) => {
110
+ try {
111
+ return res.status(200).json({
112
+ success: true,
113
+ user: {
114
+ id: req.user._id,
115
+ name: req.user.name,
116
+ email: req.user.email,
117
+ role: req.user.role,
118
+ isActive: req.user.isActive,
119
+ createdAt: req.user.createdAt,
120
+ },
121
+ });
122
+ } catch (error) {
123
+ next(error);
124
+ }
125
+ };
126
+
127
+ module.exports = {
128
+ registerUser,
129
+ loginUser,
130
+ getMe,
131
+ };