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,166 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useParams, useNavigate } from 'react-router-dom';
3
+ import { api } from '../services/api';
4
+ import Loading from '../components/Loading';
5
+
6
+ const EditResource = () => {
7
+ const { id } = useParams();
8
+ const navigate = useNavigate();
9
+
10
+ const [title, setTitle] = useState('');
11
+ const [description, setDescription] = useState('');
12
+ const [category, setCategory] = useState('General');
13
+ const [file, setFile] = useState(null);
14
+ const [existingFileName, setExistingFileName] = useState('');
15
+ const [loading, setLoading] = useState(true);
16
+ const [saving, setSaving] = useState(false);
17
+ const [error, setError] = useState(null);
18
+
19
+ useEffect(() => {
20
+ const fetchResource = async () => {
21
+ try {
22
+ const response = await api.getResourceById(id);
23
+ if (response.success) {
24
+ const res = response.data;
25
+ setTitle(res.title);
26
+ setDescription(res.description || '');
27
+ setCategory(res.category || 'General');
28
+ if (res.fileName) {
29
+ setExistingFileName(res.fileName);
30
+ }
31
+ }
32
+ } catch (err) {
33
+ setError(err.message || 'Failed to fetch resource for editing.');
34
+ } finally {
35
+ setLoading(false);
36
+ }
37
+ };
38
+
39
+ fetchResource();
40
+ }, [id]);
41
+
42
+ const handleFileChange = (e) => {
43
+ if (e.target.files && e.target.files[0]) {
44
+ setFile(e.target.files[0]);
45
+ }
46
+ };
47
+
48
+ const handleSubmit = async (e) => {
49
+ e.preventDefault();
50
+ setSaving(true);
51
+ setError(null);
52
+
53
+ try {
54
+ const formData = new FormData();
55
+ formData.append('title', title);
56
+ formData.append('description', description);
57
+ formData.append('category', category);
58
+ if (file) {
59
+ formData.append('file', file);
60
+ }
61
+
62
+ const response = await api.updateResource(id, formData);
63
+ if (response.success) {
64
+ navigate(`/resources/${id}`);
65
+ }
66
+ } catch (err) {
67
+ setError(err.message || 'Failed to update resource');
68
+ } finally {
69
+ setSaving(false);
70
+ }
71
+ };
72
+
73
+ if (loading) return <Loading message="Loading resource..." />;
74
+
75
+ return (
76
+ <div className="container" style={{ maxWidth: '650px' }}>
77
+ <div className="card">
78
+ <h2>Edit Resource</h2>
79
+ <p style={{ marginBottom: '1.5rem', fontSize: '0.9rem' }}>
80
+ Update fields or replace existing file attachment
81
+ </p>
82
+
83
+ {error && <div className="alert alert-error">{error}</div>}
84
+
85
+ <form onSubmit={handleSubmit}>
86
+ <div className="form-group">
87
+ <label htmlFor="title">Title *</label>
88
+ <input
89
+ id="title"
90
+ type="text"
91
+ required
92
+ className="form-control"
93
+ value={title}
94
+ onChange={(e) => setTitle(e.target.value)}
95
+ />
96
+ </div>
97
+
98
+ <div className="form-group">
99
+ <label htmlFor="category">Category</label>
100
+ <select
101
+ id="category"
102
+ className="form-control"
103
+ value={category}
104
+ onChange={(e) => setCategory(e.target.value)}
105
+ >
106
+ <option value="General">General</option>
107
+ <option value="Documentation">Documentation</option>
108
+ <option value="Assignment">Assignment</option>
109
+ <option value="Project">Project</option>
110
+ <option value="Exam">Exam</option>
111
+ </select>
112
+ </div>
113
+
114
+ <div className="form-group">
115
+ <label htmlFor="description">Description</label>
116
+ <textarea
117
+ id="description"
118
+ rows={4}
119
+ className="form-control"
120
+ value={description}
121
+ onChange={(e) => setDescription(e.target.value)}
122
+ />
123
+ </div>
124
+
125
+ <div className="form-group">
126
+ <label htmlFor="file">
127
+ Replace File (Optional - Leave blank to keep current)
128
+ </label>
129
+ {existingFileName && (
130
+ <p style={{ fontSize: '0.85rem', color: '#38bdf8', marginBottom: '0.5rem' }}>
131
+ Current File: {existingFileName}
132
+ </p>
133
+ )}
134
+ <input
135
+ id="file"
136
+ type="file"
137
+ className="form-control"
138
+ onChange={handleFileChange}
139
+ accept=".pdf,.png,.jpg,.jpeg,.doc,.docx"
140
+ />
141
+ </div>
142
+
143
+ <div style={{ display: 'flex', gap: '1rem', marginTop: '1.5rem' }}>
144
+ <button
145
+ type="submit"
146
+ className="btn btn-primary"
147
+ disabled={saving}
148
+ style={{ flex: 1 }}
149
+ >
150
+ {saving ? 'Saving changes...' : 'Save Changes'}
151
+ </button>
152
+ <button
153
+ type="button"
154
+ onClick={() => navigate(`/resources/${id}`)}
155
+ className="btn btn-secondary"
156
+ >
157
+ Cancel
158
+ </button>
159
+ </div>
160
+ </form>
161
+ </div>
162
+ </div>
163
+ );
164
+ };
165
+
166
+ export default EditResource;
@@ -0,0 +1,101 @@
1
+ import React, { useState } from 'react';
2
+ import { Link, useNavigate, useLocation } from 'react-router-dom';
3
+ import { useAuth } from '../context/AuthContext';
4
+
5
+ const Login = () => {
6
+ const [formData, setFormData] = useState({
7
+ email: '',
8
+ password: '',
9
+ });
10
+ const [error, setError] = useState(null);
11
+ const [loading, setLoading] = useState(false);
12
+
13
+ const { login } = useAuth();
14
+ const navigate = useNavigate();
15
+ const location = useLocation();
16
+
17
+ const from = location.state?.from?.pathname || '/dashboard';
18
+
19
+ const handleChange = (e) => {
20
+ setFormData({
21
+ ...formData,
22
+ [e.target.name]: e.target.value,
23
+ });
24
+ };
25
+
26
+ const handleSubmit = async (e) => {
27
+ e.preventDefault();
28
+ setError(null);
29
+ setLoading(true);
30
+
31
+ try {
32
+ await login(formData.email, formData.password);
33
+ navigate(from, { replace: true });
34
+ } catch (err) {
35
+ setError(err.message || 'Login failed. Please check your credentials.');
36
+ } finally {
37
+ setLoading(false);
38
+ }
39
+ };
40
+
41
+ return (
42
+ <div className="container" style={{ maxWidth: '450px', marginTop: '2rem' }}>
43
+ <div className="card">
44
+ <h2 style={{ textAlign: 'center', marginBottom: '0.5rem' }}>Welcome Back</h2>
45
+ <p style={{ textAlign: 'center', marginBottom: '1.5rem', fontSize: '0.9rem' }}>
46
+ Sign in to access your MERN dashboard
47
+ </p>
48
+
49
+ {error && <div className="alert alert-error">{error}</div>}
50
+
51
+ <form onSubmit={handleSubmit}>
52
+ <div className="form-group">
53
+ <label htmlFor="email">Email Address</label>
54
+ <input
55
+ id="email"
56
+ type="email"
57
+ name="email"
58
+ required
59
+ className="form-control"
60
+ value={formData.email}
61
+ onChange={handleChange}
62
+ placeholder="user@example.com"
63
+ />
64
+ </div>
65
+
66
+ <div className="form-group">
67
+ <label htmlFor="password">Password</label>
68
+ <input
69
+ id="password"
70
+ type="password"
71
+ name="password"
72
+ required
73
+ className="form-control"
74
+ value={formData.password}
75
+ onChange={handleChange}
76
+ placeholder="••••••••"
77
+ />
78
+ </div>
79
+
80
+ <button
81
+ type="submit"
82
+ className="btn btn-primary"
83
+ style={{ width: '100%', marginTop: '0.5rem' }}
84
+ disabled={loading}
85
+ >
86
+ {loading ? 'Signing in...' : 'Sign In'}
87
+ </button>
88
+ </form>
89
+
90
+ <p style={{ textAlign: 'center', marginTop: '1.5rem', fontSize: '0.9rem' }}>
91
+ Don't have an account?{' '}
92
+ <Link to="/register" style={{ color: 'var(--primary)' }}>
93
+ Register here
94
+ </Link>
95
+ </p>
96
+ </div>
97
+ </div>
98
+ );
99
+ };
100
+
101
+ export default Login;
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import { Link } from 'react-router-dom';
3
+
4
+ const NotFound = () => {
5
+ return (
6
+ <div className="container" style={{ textAlign: 'center', marginTop: '4rem' }}>
7
+ <div className="card" style={{ maxWidth: '500px', margin: '0 auto', padding: '3rem 2rem' }}>
8
+ <h1 style={{ fontSize: '3rem', color: 'var(--primary)', marginBottom: '0.5rem' }}>404</h1>
9
+ <h2>Page Not Found</h2>
10
+ <p style={{ margin: '1rem 0 2rem 0' }}>
11
+ The page you are looking for does not exist or has been moved.
12
+ </p>
13
+ <Link to="/" className="btn btn-primary">
14
+ Return to Home
15
+ </Link>
16
+ </div>
17
+ </div>
18
+ );
19
+ };
20
+
21
+ export default NotFound;
@@ -0,0 +1,52 @@
1
+ import React from 'react';
2
+ import { useAuth } from '../context/AuthContext';
3
+
4
+ const Profile = () => {
5
+ const { user } = useAuth();
6
+
7
+ return (
8
+ <div className="container" style={{ maxWidth: '600px', marginTop: '2rem' }}>
9
+ <div className="card">
10
+ <h2>User Profile</h2>
11
+ <p style={{ marginBottom: '1.5rem', fontSize: '0.9rem' }}>
12
+ Verified details from JWT Authentication (GET /api/auth/me)
13
+ </p>
14
+
15
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
16
+ <div>
17
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Full Name</label>
18
+ <p style={{ fontSize: '1.1rem', fontWeight: '600', color: 'var(--text-primary)' }}>{user?.name}</p>
19
+ </div>
20
+
21
+ <div>
22
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Email Address</label>
23
+ <p style={{ fontSize: '1.1rem', fontWeight: '600', color: 'var(--text-primary)' }}>{user?.email}</p>
24
+ </div>
25
+
26
+ <div>
27
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Assigned Role</label>
28
+ <p style={{ marginTop: '0.25rem' }}>
29
+ <span className="badge badge-primary">{user?.role}</span>
30
+ </p>
31
+ </div>
32
+
33
+ <div>
34
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>User ID (MongoDB ObjectId)</label>
35
+ <p style={{ fontFamily: 'monospace', fontSize: '0.9rem', color: 'var(--text-secondary)' }}>
36
+ {user?.id || user?._id}
37
+ </p>
38
+ </div>
39
+
40
+ <div>
41
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Account Created At</label>
42
+ <p style={{ fontSize: '0.9rem', color: 'var(--text-secondary)' }}>
43
+ {user?.createdAt ? new Date(user.createdAt).toLocaleString() : 'Active Session'}
44
+ </p>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ );
50
+ };
51
+
52
+ export default Profile;
@@ -0,0 +1,134 @@
1
+ import React, { useState } from 'react';
2
+ import { Link, useNavigate } from 'react-router-dom';
3
+ import { useAuth } from '../context/AuthContext';
4
+
5
+ const Register = () => {
6
+ const [formData, setFormData] = useState({
7
+ name: '',
8
+ email: '',
9
+ password: '',
10
+ role: 'student',
11
+ });
12
+ const [error, setError] = useState(null);
13
+ const [loading, setLoading] = useState(false);
14
+
15
+ const { register } = useAuth();
16
+ const navigate = useNavigate();
17
+
18
+ const handleChange = (e) => {
19
+ setFormData({
20
+ ...formData,
21
+ [e.target.name]: e.target.value,
22
+ });
23
+ };
24
+
25
+ const handleSubmit = async (e) => {
26
+ e.preventDefault();
27
+ setError(null);
28
+ setLoading(true);
29
+
30
+ try {
31
+ await register(
32
+ formData.name,
33
+ formData.email,
34
+ formData.password,
35
+ formData.role
36
+ );
37
+ navigate('/dashboard');
38
+ } catch (err) {
39
+ setError(err.message || 'Registration failed. Please check form details.');
40
+ } finally {
41
+ setLoading(false);
42
+ }
43
+ };
44
+
45
+ return (
46
+ <div className="container" style={{ maxWidth: '480px', marginTop: '2rem' }}>
47
+ <div className="card">
48
+ <h2 style={{ textAlign: 'center', marginBottom: '0.5rem' }}>Create Account</h2>
49
+ <p style={{ textAlign: 'center', marginBottom: '1.5rem', fontSize: '0.9rem' }}>
50
+ Register for student or admin privileges
51
+ </p>
52
+
53
+ {error && <div className="alert alert-error">{error}</div>}
54
+
55
+ <form onSubmit={handleSubmit}>
56
+ <div className="form-group">
57
+ <label htmlFor="name">Full Name</label>
58
+ <input
59
+ id="name"
60
+ type="text"
61
+ name="name"
62
+ required
63
+ className="form-control"
64
+ value={formData.name}
65
+ onChange={handleChange}
66
+ placeholder="John Doe"
67
+ />
68
+ </div>
69
+
70
+ <div className="form-group">
71
+ <label htmlFor="email">Email Address</label>
72
+ <input
73
+ id="email"
74
+ type="email"
75
+ name="email"
76
+ required
77
+ className="form-control"
78
+ value={formData.email}
79
+ onChange={handleChange}
80
+ placeholder="john@example.com"
81
+ />
82
+ </div>
83
+
84
+ <div className="form-group">
85
+ <label htmlFor="password">Password (min 6 characters)</label>
86
+ <input
87
+ id="password"
88
+ type="password"
89
+ name="password"
90
+ required
91
+ minLength={6}
92
+ className="form-control"
93
+ value={formData.password}
94
+ onChange={handleChange}
95
+ placeholder="••••••••"
96
+ />
97
+ </div>
98
+
99
+ <div className="form-group">
100
+ <label htmlFor="role">Role</label>
101
+ <select
102
+ id="role"
103
+ name="role"
104
+ className="form-control"
105
+ value={formData.role}
106
+ onChange={handleChange}
107
+ >
108
+ <option value="student">Student (Standard User)</option>
109
+ <option value="admin">Admin (Elevated Permissions)</option>
110
+ </select>
111
+ </div>
112
+
113
+ <button
114
+ type="submit"
115
+ className="btn btn-primary"
116
+ style={{ width: '100%', marginTop: '0.5rem' }}
117
+ disabled={loading}
118
+ >
119
+ {loading ? 'Creating account...' : 'Register'}
120
+ </button>
121
+ </form>
122
+
123
+ <p style={{ textAlign: 'center', marginTop: '1.5rem', fontSize: '0.9rem' }}>
124
+ Already have an account?{' '}
125
+ <Link to="/login" style={{ color: 'var(--primary)' }}>
126
+ Login here
127
+ </Link>
128
+ </p>
129
+ </div>
130
+ </div>
131
+ );
132
+ };
133
+
134
+ export default Register;
@@ -0,0 +1,156 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { useParams, useNavigate, Link } from 'react-router-dom';
3
+ import { api } from '../services/api';
4
+ import { useAuth } from '../context/AuthContext';
5
+ import Loading from '../components/Loading';
6
+
7
+ const ResourceDetails = () => {
8
+ const { id } = useParams();
9
+ const navigate = useNavigate();
10
+ const { user, isAuthenticated } = useAuth();
11
+
12
+ const [resource, setResource] = useState(null);
13
+ const [loading, setLoading] = useState(true);
14
+ const [error, setError] = useState(null);
15
+ const [deleteLoading, setDeleteLoading] = useState(false);
16
+
17
+ useEffect(() => {
18
+ const fetchDetails = async () => {
19
+ try {
20
+ const response = await api.getResourceById(id);
21
+ if (response.success) {
22
+ setResource(response.data);
23
+ }
24
+ } catch (err) {
25
+ setError(err.message || 'Failed to load resource details.');
26
+ } finally {
27
+ setLoading(false);
28
+ }
29
+ };
30
+
31
+ fetchDetails();
32
+ }, [id]);
33
+
34
+ const handleDelete = async () => {
35
+ if (!window.confirm('Are you sure you want to delete this resource?')) {
36
+ return;
37
+ }
38
+
39
+ setDeleteLoading(true);
40
+ try {
41
+ await api.deleteResource(id);
42
+ navigate('/resources');
43
+ } catch (err) {
44
+ alert(err.message || 'Failed to delete resource');
45
+ setDeleteLoading(false);
46
+ }
47
+ };
48
+
49
+ if (loading) return <Loading message="Loading resource details..." />;
50
+ if (error) {
51
+ return (
52
+ <div className="container" style={{ textAlign: 'center', marginTop: '3rem' }}>
53
+ <div className="card">
54
+ <h2>Error</h2>
55
+ <p>{error}</p>
56
+ <Link to="/resources" className="btn btn-secondary" style={{ marginTop: '1rem' }}>
57
+ Back to Resources
58
+ </Link>
59
+ </div>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ const isOwner =
65
+ isAuthenticated &&
66
+ user &&
67
+ resource?.createdBy &&
68
+ (resource.createdBy._id === user.id || resource.createdBy === user.id);
69
+ const isAdmin = user?.role === 'admin';
70
+ const canModify = isOwner || isAdmin;
71
+
72
+ return (
73
+ <div className="container" style={{ maxWidth: '800px' }}>
74
+ <Link to="/resources" style={{ color: 'var(--text-secondary)', textDecoration: 'none', marginBottom: '1rem', display: 'inline-block' }}>
75
+ &larr; Back to all resources
76
+ </Link>
77
+
78
+ <div className="card">
79
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '1rem' }}>
80
+ <div>
81
+ <span className="badge badge-primary">{resource.category}</span>
82
+ <h1 style={{ marginTop: '0.5rem', fontSize: '1.8rem' }}>{resource.title}</h1>
83
+ </div>
84
+ {canModify && (
85
+ <div style={{ display: 'flex', gap: '0.5rem' }}>
86
+ <Link to={`/resources/${resource._id}/edit`} className="btn btn-secondary btn-sm">
87
+ Edit
88
+ </Link>
89
+ <button
90
+ onClick={handleDelete}
91
+ className="btn btn-danger btn-sm"
92
+ disabled={deleteLoading}
93
+ >
94
+ {deleteLoading ? 'Deleting...' : 'Delete'}
95
+ </button>
96
+ </div>
97
+ )}
98
+ </div>
99
+
100
+ <p style={{ fontSize: '1.05rem', whiteSpace: 'pre-line', color: 'var(--text-primary)', margin: '1.5rem 0' }}>
101
+ {resource.description || 'No description provided.'}
102
+ </p>
103
+
104
+ {resource.fileUrl && (
105
+ <div
106
+ style={{
107
+ background: 'rgba(255, 255, 255, 0.05)',
108
+ padding: '1.25rem',
109
+ borderRadius: '6px',
110
+ border: '1px solid var(--card-border)',
111
+ marginBottom: '1.5rem',
112
+ }}
113
+ >
114
+ <h4 style={{ marginBottom: '0.5rem' }}>Attached File:</h4>
115
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
116
+ <div>
117
+ <p style={{ color: '#fff', fontWeight: '500' }}>📄 {resource.fileName || 'Attached document'}</p>
118
+ {resource.fileSize && (
119
+ <p style={{ fontSize: '0.8rem' }}>Size: {(resource.fileSize / 1024).toFixed(1)} KB</p>
120
+ )}
121
+ </div>
122
+ <a
123
+ href={`http://localhost:5000/api/resources/${resource._id}/download`}
124
+ target="_blank"
125
+ rel="noreferrer"
126
+ className="btn btn-primary btn-sm"
127
+ >
128
+ Download File
129
+ </a>
130
+ </div>
131
+ </div>
132
+ )}
133
+
134
+ <div
135
+ style={{
136
+ borderTop: '1px solid var(--card-border)',
137
+ paddingTop: '1rem',
138
+ display: 'flex',
139
+ justifyContent: 'space-between',
140
+ fontSize: '0.85rem',
141
+ color: 'var(--text-secondary)',
142
+ }}
143
+ >
144
+ <div>
145
+ <span>Created by: <strong>{resource.createdBy?.name || 'Unknown'}</strong> ({resource.createdBy?.email})</span>
146
+ </div>
147
+ <div>
148
+ <span>Date: {new Date(resource.createdAt).toLocaleString()}</span>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ );
154
+ };
155
+
156
+ export default ResourceDetails;