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,138 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const bcrypt = require('bcryptjs');
|
|
4
|
+
const User = require('../models/User');
|
|
5
|
+
const Resource = require('../models/Resource');
|
|
6
|
+
const { sessionAuth } = require('../middleware/sessionAuthMiddleware');
|
|
7
|
+
|
|
8
|
+
// GET /admin/login - Render Admin Login Page
|
|
9
|
+
router.get('/login', (req, res) => {
|
|
10
|
+
if (req.session && req.session.user && req.session.user.role === 'admin') {
|
|
11
|
+
return res.redirect('/admin/dashboard');
|
|
12
|
+
}
|
|
13
|
+
res.render('login', {
|
|
14
|
+
error: req.query.error || null,
|
|
15
|
+
success: req.query.success || null,
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// POST /admin/login - Authenticate Admin User and establish Session
|
|
20
|
+
router.post('/login', async (req, res, next) => {
|
|
21
|
+
try {
|
|
22
|
+
const { email, password } = req.body;
|
|
23
|
+
|
|
24
|
+
if (!email || !password) {
|
|
25
|
+
return res.render('login', {
|
|
26
|
+
error: 'Please provide both email and password.',
|
|
27
|
+
success: null,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const user = await User.findOne({ email: email.toLowerCase() });
|
|
32
|
+
|
|
33
|
+
if (!user) {
|
|
34
|
+
return res.render('login', {
|
|
35
|
+
error: 'Invalid admin credentials.',
|
|
36
|
+
success: null,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (user.role !== 'admin') {
|
|
41
|
+
return res.render('login', {
|
|
42
|
+
error: 'Access denied: Admin role required.',
|
|
43
|
+
success: null,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!user.isActive) {
|
|
48
|
+
return res.render('login', {
|
|
49
|
+
error: 'This admin account has been deactivated.',
|
|
50
|
+
success: null,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isMatch = await bcrypt.compare(password, user.password);
|
|
55
|
+
if (!isMatch) {
|
|
56
|
+
return res.render('login', {
|
|
57
|
+
error: 'Invalid admin credentials.',
|
|
58
|
+
success: null,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Save user info in express-session
|
|
63
|
+
req.session.user = {
|
|
64
|
+
id: user._id,
|
|
65
|
+
name: user.name,
|
|
66
|
+
email: user.email,
|
|
67
|
+
role: user.role,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
res.redirect('/admin/dashboard');
|
|
71
|
+
} catch (error) {
|
|
72
|
+
next(error);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// POST /admin/logout - Destroy Session
|
|
77
|
+
router.post('/logout', (req, res) => {
|
|
78
|
+
req.session.destroy((err) => {
|
|
79
|
+
if (err) {
|
|
80
|
+
console.error('Session destroy error:', err);
|
|
81
|
+
}
|
|
82
|
+
res.redirect('/admin/login?success=Logged%20out%20successfully');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// GET /admin/dashboard - Render Protected Admin Dashboard
|
|
87
|
+
router.get('/dashboard', sessionAuth, async (req, res, next) => {
|
|
88
|
+
try {
|
|
89
|
+
const users = await User.find().sort({ createdAt: -1 });
|
|
90
|
+
const resources = await Resource.find()
|
|
91
|
+
.populate('createdBy', 'name email')
|
|
92
|
+
.sort({ createdAt: -1 });
|
|
93
|
+
|
|
94
|
+
const totalStudents = users.filter((u) => u.role === 'student').length;
|
|
95
|
+
const totalAdmins = users.filter((u) => u.role === 'admin').length;
|
|
96
|
+
|
|
97
|
+
res.render('dashboard', {
|
|
98
|
+
adminUser: req.session.user,
|
|
99
|
+
users,
|
|
100
|
+
resources,
|
|
101
|
+
stats: {
|
|
102
|
+
totalUsers: users.length,
|
|
103
|
+
totalStudents,
|
|
104
|
+
totalAdmins,
|
|
105
|
+
totalResources: resources.length,
|
|
106
|
+
},
|
|
107
|
+
message: req.query.message || null,
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
next(error);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// POST /admin/users/:id/toggle - Toggle user active status (demonstrates admin action in EJS)
|
|
115
|
+
router.post('/users/:id/toggle', sessionAuth, async (req, res, next) => {
|
|
116
|
+
try {
|
|
117
|
+
const user = await User.findById(req.params.id);
|
|
118
|
+
if (user) {
|
|
119
|
+
user.isActive = !user.isActive;
|
|
120
|
+
await user.save();
|
|
121
|
+
}
|
|
122
|
+
res.redirect('/admin/dashboard?message=User%20status%20updated');
|
|
123
|
+
} catch (error) {
|
|
124
|
+
next(error);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// POST /admin/resources/:id/delete - Admin delete resource
|
|
129
|
+
router.post('/resources/:id/delete', sessionAuth, async (req, res, next) => {
|
|
130
|
+
try {
|
|
131
|
+
await Resource.findByIdAndDelete(req.params.id);
|
|
132
|
+
res.redirect('/admin/dashboard?message=Resource%20deleted%20by%20admin');
|
|
133
|
+
} catch (error) {
|
|
134
|
+
next(error);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
module.exports = router;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const {
|
|
4
|
+
registerUser,
|
|
5
|
+
loginUser,
|
|
6
|
+
getMe,
|
|
7
|
+
} = require('../controllers/authController');
|
|
8
|
+
const { protect } = require('../middleware/authMiddleware');
|
|
9
|
+
const {
|
|
10
|
+
registerValidationRules,
|
|
11
|
+
loginValidationRules,
|
|
12
|
+
} = require('../middleware/validateMiddleware');
|
|
13
|
+
|
|
14
|
+
// Public routes
|
|
15
|
+
router.post('/register', registerValidationRules, registerUser);
|
|
16
|
+
router.post('/login', loginValidationRules, loginUser);
|
|
17
|
+
|
|
18
|
+
// Protected routes
|
|
19
|
+
router.get('/me', protect, getMe);
|
|
20
|
+
|
|
21
|
+
module.exports = router;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const {
|
|
4
|
+
getResources,
|
|
5
|
+
getResourceById,
|
|
6
|
+
createResource,
|
|
7
|
+
updateResource,
|
|
8
|
+
deleteResource,
|
|
9
|
+
downloadResourceFile,
|
|
10
|
+
} = require('../controllers/resourceController');
|
|
11
|
+
const { protect } = require('../middleware/authMiddleware');
|
|
12
|
+
const upload = require('../middleware/uploadMiddleware');
|
|
13
|
+
const {
|
|
14
|
+
resourceValidationRules,
|
|
15
|
+
} = require('../middleware/validateMiddleware');
|
|
16
|
+
|
|
17
|
+
// Public read routes
|
|
18
|
+
router.get('/', getResources);
|
|
19
|
+
router.get('/:id', getResourceById);
|
|
20
|
+
router.get('/:id/download', downloadResourceFile);
|
|
21
|
+
|
|
22
|
+
// Protected mutation routes (with optional Multer file upload)
|
|
23
|
+
router.post('/', protect, upload.single('file'), resourceValidationRules, createResource);
|
|
24
|
+
router.put('/:id', protect, upload.single('file'), updateResource);
|
|
25
|
+
router.delete('/:id', protect, deleteResource);
|
|
26
|
+
|
|
27
|
+
module.exports = router;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const dotenv = require('dotenv');
|
|
4
|
+
const cors = require('cors');
|
|
5
|
+
const session = require('express-session');
|
|
6
|
+
const connectDB = require('./config/db');
|
|
7
|
+
const { notFound, errorHandler } = require('./middleware/errorMiddleware');
|
|
8
|
+
|
|
9
|
+
// Load environment variables
|
|
10
|
+
dotenv.config();
|
|
11
|
+
|
|
12
|
+
// Connect to MongoDB
|
|
13
|
+
connectDB();
|
|
14
|
+
|
|
15
|
+
const app = express();
|
|
16
|
+
|
|
17
|
+
// Body Parser Middleware
|
|
18
|
+
app.use(express.json());
|
|
19
|
+
app.use(express.urlencoded({ extended: true }));
|
|
20
|
+
|
|
21
|
+
// CORS configuration (allow client requests)
|
|
22
|
+
app.use(
|
|
23
|
+
cors({
|
|
24
|
+
origin: '*',
|
|
25
|
+
credentials: true,
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Express Session Configuration (For EJS Admin Portal)
|
|
30
|
+
app.use(
|
|
31
|
+
session({
|
|
32
|
+
secret: process.env.SESSION_SECRET || 'mern_exam_session_secret_key_12345',
|
|
33
|
+
resave: false,
|
|
34
|
+
saveUninitialized: false,
|
|
35
|
+
cookie: {
|
|
36
|
+
httpOnly: true,
|
|
37
|
+
maxAge: 24 * 60 * 60 * 1000, // 1 day
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// EJS View Engine Setup
|
|
43
|
+
app.set('view engine', 'ejs');
|
|
44
|
+
app.set('views', path.join(__dirname, 'views'));
|
|
45
|
+
|
|
46
|
+
// Static Folders
|
|
47
|
+
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
|
|
48
|
+
app.use(express.static(path.join(__dirname, 'public')));
|
|
49
|
+
|
|
50
|
+
// API Routes
|
|
51
|
+
app.use('/api/auth', require('./routes/authRoutes'));
|
|
52
|
+
app.use('/api/resources', require('./routes/resourceRoutes'));
|
|
53
|
+
|
|
54
|
+
// EJS Admin Routes
|
|
55
|
+
app.use('/admin', require('./routes/adminRoutes'));
|
|
56
|
+
|
|
57
|
+
// Health check route
|
|
58
|
+
app.get('/api/health', (req, res) => {
|
|
59
|
+
res.status(200).json({
|
|
60
|
+
status: 'ok',
|
|
61
|
+
timestamp: new Date().toISOString(),
|
|
62
|
+
uptime: process.uptime(),
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Root route redirect / info
|
|
67
|
+
app.get('/', (req, res) => {
|
|
68
|
+
res.send(`
|
|
69
|
+
<div style="font-family: sans-serif; padding: 2rem; max-width: 600px; margin: 0 auto; text-align: center;">
|
|
70
|
+
<h2>🚀 MERN Template Backend Server Running</h2>
|
|
71
|
+
<p style="color: #64748b; margin: 1rem 0;">Exam-ready architecture with JWT API & EJS Admin portal.</p>
|
|
72
|
+
<div style="display: flex; gap: 1rem; justify-content: center; margin-top: 1.5rem;">
|
|
73
|
+
<a href="/admin/login" style="padding: 0.6rem 1.2rem; background: #4f46e5; color: white; text-decoration: none; border-radius: 6px; font-weight: 500;">Admin EJS Panel</a>
|
|
74
|
+
<a href="/api/health" style="padding: 0.6rem 1.2rem; background: #0f172a; color: white; text-decoration: none; border-radius: 6px; font-weight: 500;">API Health</a>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
`);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Error handling middleware
|
|
81
|
+
app.use(notFound);
|
|
82
|
+
app.use(errorHandler);
|
|
83
|
+
|
|
84
|
+
const PORT = process.env.PORT || 5000;
|
|
85
|
+
|
|
86
|
+
app.listen(PORT, () => {
|
|
87
|
+
console.log(`[Express Server] Server running on http://localhost:${PORT}`);
|
|
88
|
+
console.log(`[EJS Admin Portal] Admin portal at http://localhost:${PORT}/admin/login`);
|
|
89
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const jwt = require('jsonwebtoken');
|
|
2
|
+
|
|
3
|
+
const generateToken = (user) => {
|
|
4
|
+
return jwt.sign(
|
|
5
|
+
{
|
|
6
|
+
id: user._id,
|
|
7
|
+
role: user.role,
|
|
8
|
+
},
|
|
9
|
+
process.env.JWT_SECRET || 'fallback_jwt_secret_for_exam',
|
|
10
|
+
{
|
|
11
|
+
expiresIn: '1d',
|
|
12
|
+
}
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
generateToken,
|
|
18
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<%- include('partials/header', { title: 'Admin Dashboard - MERN Exam' }) %>
|
|
2
|
+
|
|
3
|
+
<h2 style="margin-bottom: 1.5rem;">System Overview & Statistics</h2>
|
|
4
|
+
|
|
5
|
+
<% if (typeof message !== 'undefined' && message) { %>
|
|
6
|
+
<div class="alert alert-success"><%= message %></div>
|
|
7
|
+
<% } %>
|
|
8
|
+
|
|
9
|
+
<!-- Stats Grid -->
|
|
10
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem;">
|
|
11
|
+
<div class="card" style="margin-bottom: 0; text-align: center;">
|
|
12
|
+
<p style="color: var(--text-muted); font-size: 0.85rem;">Total Users</p>
|
|
13
|
+
<h3 style="font-size: 1.8rem; color: var(--primary);"><%= stats.totalUsers %></h3>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="card" style="margin-bottom: 0; text-align: center;">
|
|
16
|
+
<p style="color: var(--text-muted); font-size: 0.85rem;">Students</p>
|
|
17
|
+
<h3 style="font-size: 1.8rem; color: #10b981;"><%= stats.totalStudents %></h3>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="card" style="margin-bottom: 0; text-align: center;">
|
|
20
|
+
<p style="color: var(--text-muted); font-size: 0.85rem;">Admins</p>
|
|
21
|
+
<h3 style="font-size: 1.8rem; color: #6366f1;"><%= stats.totalAdmins %></h3>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="card" style="margin-bottom: 0; text-align: center;">
|
|
24
|
+
<p style="color: var(--text-muted); font-size: 0.85rem;">Total Resources</p>
|
|
25
|
+
<h3 style="font-size: 1.8rem; color: #f59e0b;"><%= stats.totalResources %></h3>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<!-- Users Management Table -->
|
|
30
|
+
<div class="card">
|
|
31
|
+
<h3>Registered Users</h3>
|
|
32
|
+
<p style="color: var(--text-muted); font-size: 0.85rem; margin-bottom: 1rem;">
|
|
33
|
+
Demonstrates EJS looping, conditionals, and server-side state toggling.
|
|
34
|
+
</p>
|
|
35
|
+
<table>
|
|
36
|
+
<thead>
|
|
37
|
+
<tr>
|
|
38
|
+
<th>Name</th>
|
|
39
|
+
<th>Email</th>
|
|
40
|
+
<th>Role</th>
|
|
41
|
+
<th>Status</th>
|
|
42
|
+
<th>Joined</th>
|
|
43
|
+
<th>Actions</th>
|
|
44
|
+
</tr>
|
|
45
|
+
</thead>
|
|
46
|
+
<tbody>
|
|
47
|
+
<% if (users.length === 0) { %>
|
|
48
|
+
<tr>
|
|
49
|
+
<td colspan="6" style="text-align: center; color: var(--text-muted);">No users registered yet.</td>
|
|
50
|
+
</tr>
|
|
51
|
+
<% } else { %>
|
|
52
|
+
<% users.forEach(function(u) { %>
|
|
53
|
+
<tr>
|
|
54
|
+
<td><strong><%= u.name %></strong></td>
|
|
55
|
+
<td><%= u.email %></td>
|
|
56
|
+
<td>
|
|
57
|
+
<span class="badge <%= u.role === 'admin' ? 'badge-admin' : 'badge-student' %>">
|
|
58
|
+
<%= u.role %>
|
|
59
|
+
</span>
|
|
60
|
+
</td>
|
|
61
|
+
<td>
|
|
62
|
+
<span class="badge <%= u.isActive ? 'badge-active' : 'badge-inactive' %>">
|
|
63
|
+
<%= u.isActive ? 'Active' : 'Inactive' %>
|
|
64
|
+
</span>
|
|
65
|
+
</td>
|
|
66
|
+
<td><%= new Date(u.createdAt).toLocaleDateString() %></td>
|
|
67
|
+
<td>
|
|
68
|
+
<form action="/admin/users/<%= u._id %>/toggle" method="POST" style="display:inline;">
|
|
69
|
+
<button type="submit" class="btn btn-sm <%= u.isActive ? 'btn-danger' : 'btn-primary' %>">
|
|
70
|
+
<%= u.isActive ? 'Deactivate' : 'Activate' %>
|
|
71
|
+
</button>
|
|
72
|
+
</form>
|
|
73
|
+
</td>
|
|
74
|
+
</tr>
|
|
75
|
+
<% }); %>
|
|
76
|
+
<% } %>
|
|
77
|
+
</tbody>
|
|
78
|
+
</table>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- Resources Table -->
|
|
82
|
+
<div class="card">
|
|
83
|
+
<h3>Created Resources</h3>
|
|
84
|
+
<table>
|
|
85
|
+
<thead>
|
|
86
|
+
<tr>
|
|
87
|
+
<th>Title</th>
|
|
88
|
+
<th>Category</th>
|
|
89
|
+
<th>Created By</th>
|
|
90
|
+
<th>File Attachment</th>
|
|
91
|
+
<th>Created At</th>
|
|
92
|
+
<th>Actions</th>
|
|
93
|
+
</tr>
|
|
94
|
+
</thead>
|
|
95
|
+
<tbody>
|
|
96
|
+
<% if (resources.length === 0) { %>
|
|
97
|
+
<tr>
|
|
98
|
+
<td colspan="6" style="text-align: center; color: var(--text-muted);">No resources found.</td>
|
|
99
|
+
</tr>
|
|
100
|
+
<% } else { %>
|
|
101
|
+
<% resources.forEach(function(r) { %>
|
|
102
|
+
<tr>
|
|
103
|
+
<td><strong><%= r.title %></strong></td>
|
|
104
|
+
<td><%= r.category %></td>
|
|
105
|
+
<td><%= r.createdBy ? r.createdBy.name : 'Unknown' %></td>
|
|
106
|
+
<td>
|
|
107
|
+
<% if (r.fileUrl) { %>
|
|
108
|
+
<a href="/api/resources/<%= r._id %>/download" target="_blank" style="color: var(--primary);">
|
|
109
|
+
📥 <%= r.fileName || 'Download' %>
|
|
110
|
+
</a>
|
|
111
|
+
<% } else { %>
|
|
112
|
+
<span style="color: var(--text-muted);">None</span>
|
|
113
|
+
<% } %>
|
|
114
|
+
</td>
|
|
115
|
+
<td><%= new Date(r.createdAt).toLocaleDateString() %></td>
|
|
116
|
+
<td>
|
|
117
|
+
<form action="/admin/resources/<%= r._id %>/delete" method="POST" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete this resource?');">
|
|
118
|
+
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
|
119
|
+
</form>
|
|
120
|
+
</td>
|
|
121
|
+
</tr>
|
|
122
|
+
<% }); %>
|
|
123
|
+
<% } %>
|
|
124
|
+
</tbody>
|
|
125
|
+
</table>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<%- include('partials/footer') %>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<%- include('partials/header', { title: 'Admin Login - MERN Exam' }) %>
|
|
2
|
+
|
|
3
|
+
<div style="max-width: 420px; margin: 3rem auto;">
|
|
4
|
+
<div class="card">
|
|
5
|
+
<h2 style="margin-bottom: 0.5rem; text-align: center;">Admin Sign In</h2>
|
|
6
|
+
<p style="color: var(--text-muted); font-size: 0.875rem; text-align: center; margin-bottom: 1.5rem;">
|
|
7
|
+
Server-rendered session authentication demo
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<% if (typeof error !== 'undefined' && error) { %>
|
|
11
|
+
<div class="alert alert-error"><%= error %></div>
|
|
12
|
+
<% } %>
|
|
13
|
+
|
|
14
|
+
<% if (typeof success !== 'undefined' && success) { %>
|
|
15
|
+
<div class="alert alert-success"><%= success %></div>
|
|
16
|
+
<% } %>
|
|
17
|
+
|
|
18
|
+
<form action="/admin/login" method="POST">
|
|
19
|
+
<div style="margin-bottom: 1rem;">
|
|
20
|
+
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.875rem; font-weight: 500;">Admin Email</label>
|
|
21
|
+
<input
|
|
22
|
+
type="email"
|
|
23
|
+
name="email"
|
|
24
|
+
required
|
|
25
|
+
placeholder="admin@example.com"
|
|
26
|
+
style="width: 100%; padding: 0.6rem; border: 1px solid var(--border); border-radius: 6px; font-size: 0.9rem;"
|
|
27
|
+
/>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div style="margin-bottom: 1.5rem;">
|
|
31
|
+
<label style="display: block; margin-bottom: 0.5rem; font-size: 0.875rem; font-weight: 500;">Password</label>
|
|
32
|
+
<input
|
|
33
|
+
type="password"
|
|
34
|
+
name="password"
|
|
35
|
+
required
|
|
36
|
+
placeholder="••••••••"
|
|
37
|
+
style="width: 100%; padding: 0.6rem; border: 1px solid var(--border); border-radius: 6px; font-size: 0.9rem;"
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<button type="submit" class="btn btn-primary" style="width: 100%; padding: 0.75rem;">
|
|
42
|
+
Log In to Admin Panel
|
|
43
|
+
</button>
|
|
44
|
+
</form>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<%- include('partials/footer') %>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title><%= typeof title !== 'undefined' ? title : 'MERN Admin Portal' %></title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--primary: #4f46e5;
|
|
10
|
+
--primary-hover: #4338ca;
|
|
11
|
+
--bg: #f8fafc;
|
|
12
|
+
--card-bg: #ffffff;
|
|
13
|
+
--text: #1e293b;
|
|
14
|
+
--text-muted: #64748b;
|
|
15
|
+
--border: #e2e8f0;
|
|
16
|
+
--danger: #ef4444;
|
|
17
|
+
--success: #10b981;
|
|
18
|
+
}
|
|
19
|
+
* { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
|
20
|
+
body { background-color: var(--bg); color: var(--text); min-height: 100vh; display: flex; flex-direction: column; }
|
|
21
|
+
header { background: #1e1e2e; color: #fff; padding: 1rem 2rem; display: flex; justify-content: space-between; align-items: center; }
|
|
22
|
+
header h1 { font-size: 1.25rem; font-weight: 700; }
|
|
23
|
+
header .nav-links { display: flex; gap: 1rem; align-items: center; }
|
|
24
|
+
header a, header button { color: #fff; text-decoration: none; font-size: 0.9rem; }
|
|
25
|
+
.btn { padding: 0.5rem 1rem; border-radius: 6px; font-weight: 500; cursor: pointer; border: none; transition: 0.2s; }
|
|
26
|
+
.btn-primary { background: var(--primary); color: #fff; }
|
|
27
|
+
.btn-primary:hover { background: var(--primary-hover); }
|
|
28
|
+
.btn-danger { background: var(--danger); color: #fff; }
|
|
29
|
+
.btn-sm { padding: 0.25rem 0.5rem; font-size: 0.8rem; }
|
|
30
|
+
.container { max-width: 1100px; margin: 2rem auto; padding: 0 1rem; flex: 1; }
|
|
31
|
+
.card { background: var(--card-bg); border-radius: 8px; border: 1px solid var(--border); padding: 1.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.05); margin-bottom: 1.5rem; }
|
|
32
|
+
.alert { padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: 0.9rem; }
|
|
33
|
+
.alert-error { background: #fee2e2; color: #991b1b; border: 1px solid #f87171; }
|
|
34
|
+
.alert-success { background: #d1fae5; color: #065f46; border: 1px solid #34d399; }
|
|
35
|
+
table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
|
|
36
|
+
th, td { text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--border); font-size: 0.9rem; }
|
|
37
|
+
th { background: #f1f5f9; color: var(--text-muted); font-weight: 600; }
|
|
38
|
+
.badge { display: inline-block; padding: 0.2rem 0.5rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; }
|
|
39
|
+
.badge-admin { background: #e0e7ff; color: #4338ca; }
|
|
40
|
+
.badge-student { background: #f1f5f9; color: #475569; }
|
|
41
|
+
.badge-active { background: #d1fae5; color: #065f46; }
|
|
42
|
+
.badge-inactive { background: #fee2e2; color: #991b1b; }
|
|
43
|
+
footer { background: #fff; border-top: 1px solid var(--border); padding: 1rem; text-align: center; color: var(--text-muted); font-size: 0.85rem; margin-top: auto; }
|
|
44
|
+
</style>
|
|
45
|
+
</head>
|
|
46
|
+
<body>
|
|
47
|
+
<header>
|
|
48
|
+
<h1>âš¡ MERN Admin Portal (EJS Session Auth)</h1>
|
|
49
|
+
<div class="nav-links">
|
|
50
|
+
<% if (typeof adminUser !== 'undefined' && adminUser) { %>
|
|
51
|
+
<span>Logged in as: <strong><%= adminUser.name %></strong> (<%= adminUser.role %>)</span>
|
|
52
|
+
<form action="/admin/logout" method="POST" style="display:inline;">
|
|
53
|
+
<button type="submit" class="btn btn-danger btn-sm">Logout</button>
|
|
54
|
+
</form>
|
|
55
|
+
<% } else { %>
|
|
56
|
+
<a href="/admin/login">Admin Login</a>
|
|
57
|
+
<% } %>
|
|
58
|
+
</div>
|
|
59
|
+
</header>
|
|
60
|
+
<main class="container">
|