QOL improvements
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 28s
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 28s
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { requireAdmin } = require('../middleware/auth');
|
||||
const { Hunts, Packages } = require('../models');
|
||||
const { Hunts, Packages, Users } = require('../models');
|
||||
const { generateHuntPDF } = require('../utils/pdf');
|
||||
|
||||
// All admin routes require admin access
|
||||
@@ -10,7 +10,8 @@ router.use(requireAdmin);
|
||||
// Admin dashboard
|
||||
router.get('/', (req, res) => {
|
||||
const hunts = Hunts.getByCreator(req.session.userId);
|
||||
res.render('admin/dashboard', { title: 'Admin Dashboard', hunts });
|
||||
const users = Users.getAllUsers();
|
||||
res.render('admin/dashboard', { title: 'Admin Dashboard', hunts, users, resetUrl: null, resetUsername: null });
|
||||
});
|
||||
|
||||
// Create hunt form
|
||||
@@ -71,7 +72,8 @@ router.get('/hunts/:id', (req, res) => {
|
||||
if (!hunt) return res.status(404).render('error', { title: 'Not Found', message: 'Hunt not found.' });
|
||||
|
||||
const packages = Packages.getByHunt(hunt.id);
|
||||
res.render('admin/manage-hunt', { title: `Manage: ${hunt.name}`, hunt, packages });
|
||||
const baseUrl = process.env.BASE_URL || `http://localhost:${process.env.PORT || 3000}`;
|
||||
res.render('admin/manage-hunt', { title: `Manage: ${hunt.name}`, hunt, packages, baseUrl });
|
||||
});
|
||||
|
||||
// Download PDF of QR codes
|
||||
@@ -93,4 +95,31 @@ router.get('/hunts/:id/pdf', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Generate password reset URL ──────────────────────────
|
||||
router.post('/reset-password', (req, res) => {
|
||||
const { username } = req.body;
|
||||
const user = Users.findByUsername(username);
|
||||
|
||||
if (!user) {
|
||||
const hunts = Hunts.getByCreator(req.session.userId);
|
||||
const users = Users.getAllUsers();
|
||||
req.session.flash = { type: 'danger', message: `User "${username}" not found.` };
|
||||
return res.redirect('/admin');
|
||||
}
|
||||
|
||||
const token = Users.createPasswordResetToken(user.id);
|
||||
const baseUrl = process.env.BASE_URL || `http://localhost:${process.env.PORT || 3000}`;
|
||||
const resetUrl = `${baseUrl}/auth/reset/${token}`;
|
||||
|
||||
const hunts = Hunts.getByCreator(req.session.userId);
|
||||
const users = Users.getAllUsers();
|
||||
res.render('admin/dashboard', {
|
||||
title: 'Admin Dashboard',
|
||||
hunts,
|
||||
users,
|
||||
resetUrl,
|
||||
resetUsername: username
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -79,4 +79,39 @@ router.get('/logout', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Password reset (via admin-generated URL) ────────────
|
||||
router.get('/reset/:token', (req, res) => {
|
||||
const tokenRecord = Users.findByResetToken(req.params.token);
|
||||
if (!tokenRecord) {
|
||||
return res.render('error', { title: 'Invalid Link', message: 'This password reset link is invalid or has expired.' });
|
||||
}
|
||||
res.render('auth/reset', { title: 'Reset Password', token: req.params.token, username: tokenRecord.username, error: null });
|
||||
});
|
||||
|
||||
router.post('/reset/:token', (req, res) => {
|
||||
const tokenRecord = Users.findByResetToken(req.params.token);
|
||||
if (!tokenRecord) {
|
||||
return res.render('error', { title: 'Invalid Link', message: 'This password reset link is invalid or has expired.' });
|
||||
}
|
||||
|
||||
const { password, password_confirm } = req.body;
|
||||
|
||||
if (!password || password.length < 6) {
|
||||
return res.render('auth/reset', { title: 'Reset Password', token: req.params.token, username: tokenRecord.username, error: 'Password must be at least 6 characters.' });
|
||||
}
|
||||
|
||||
if (password !== password_confirm) {
|
||||
return res.render('auth/reset', { title: 'Reset Password', token: req.params.token, username: tokenRecord.username, error: 'Passwords do not match.' });
|
||||
}
|
||||
|
||||
Users.setPassword(tokenRecord.user_id, password);
|
||||
Users.consumeResetToken(req.params.token);
|
||||
|
||||
// Log the user in
|
||||
req.session.userId = tokenRecord.user_id;
|
||||
req.session.username = tokenRecord.username;
|
||||
req.session.isAdmin = false; // they can re-check on next load
|
||||
res.redirect('/');
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -48,6 +48,7 @@ router.get('/hunt/:shortName/:cardNumber', (req, res) => {
|
||||
const scanHistory = Packages.getScanHistory(pkg.id);
|
||||
const isFirstScanner = req.session && req.session.userId && fullPkg.first_scanned_by === req.session.userId;
|
||||
const isLastScanner = req.session && req.session.userId && fullPkg.last_scanned_by === req.session.userId;
|
||||
const isAdmin = !!(req.session && req.session.isAdmin);
|
||||
|
||||
res.render('loot/profile', {
|
||||
title: `Package ${fullPkg.card_number} of ${pkg.package_count} - ${fullPkg.hunt_name}`,
|
||||
@@ -55,6 +56,7 @@ router.get('/hunt/:shortName/:cardNumber', (req, res) => {
|
||||
scanHistory,
|
||||
isFirstScanner,
|
||||
isLastScanner,
|
||||
isAdmin,
|
||||
packages_total: pkg.package_count
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,6 +64,7 @@ router.get('/:shortName/:code', (req, res) => {
|
||||
const hunt = Hunts.findById(fullPkg.hunt_id);
|
||||
const isFirstScanner = fullPkg.first_scanned_by === req.session.userId;
|
||||
const isLastScanner = fullPkg.last_scanned_by === req.session.userId;
|
||||
const isAdmin = !!(req.session && req.session.isAdmin);
|
||||
|
||||
res.render('loot/scanned', {
|
||||
title: `Package ${fullPkg.card_number} of ${hunt.package_count} - ${fullPkg.hunt_name}`,
|
||||
@@ -72,6 +73,7 @@ router.get('/:shortName/:code', (req, res) => {
|
||||
scanHistory,
|
||||
isFirstScanner,
|
||||
isLastScanner,
|
||||
isAdmin,
|
||||
packages_total: hunt.package_count
|
||||
});
|
||||
});
|
||||
@@ -103,11 +105,43 @@ router.post('/:shortName/:code/image', requireAuth, upload.single('image'), (req
|
||||
return res.redirect(`/hunt/${shortName}/${pkg.card_number}`);
|
||||
}
|
||||
|
||||
// Remove old image file if replacing
|
||||
if (pkg.first_scan_image) {
|
||||
const oldPath = path.resolve(uploadsDir, path.basename(pkg.first_scan_image));
|
||||
try { fs.unlinkSync(oldPath); } catch (e) { /* file may not exist */ }
|
||||
}
|
||||
|
||||
const imagePath = `/uploads/${req.file.filename}`;
|
||||
Packages.updateFirstScanImage(pkg.id, imagePath);
|
||||
res.redirect(`/hunt/${shortName}/${pkg.card_number}`);
|
||||
});
|
||||
|
||||
// ─── Delete/remove image (first scanner or admin) ────────
|
||||
router.post('/:shortName/:code/image/delete', requireAuth, (req, res) => {
|
||||
const { shortName, code } = req.params;
|
||||
const pkg = Packages.findByHuntAndCode(shortName, code);
|
||||
|
||||
if (!pkg) {
|
||||
return res.status(404).render('error', { title: 'Not Found', message: 'Package not found.' });
|
||||
}
|
||||
|
||||
const isOwner = pkg.first_scanned_by === req.session.userId;
|
||||
const isAdmin = !!(req.session && req.session.isAdmin);
|
||||
|
||||
if (!isOwner && !isAdmin) {
|
||||
return res.status(403).render('error', { title: 'Forbidden', message: 'You cannot delete this image.' });
|
||||
}
|
||||
|
||||
// Remove file from disk
|
||||
if (pkg.first_scan_image) {
|
||||
const filePath = path.resolve(uploadsDir, path.basename(pkg.first_scan_image));
|
||||
try { fs.unlinkSync(filePath); } catch (e) { /* file may not exist */ }
|
||||
}
|
||||
|
||||
Packages.removeFirstScanImage(pkg.id);
|
||||
res.redirect(`/hunt/${shortName}/${pkg.card_number}`);
|
||||
});
|
||||
|
||||
// ─── Update hint/message ──────────────────────────────────
|
||||
router.post('/:shortName/:code/hint', requireAuth, (req, res) => {
|
||||
const { shortName, code } = req.params;
|
||||
@@ -126,4 +160,18 @@ router.post('/:shortName/:code/hint', requireAuth, (req, res) => {
|
||||
res.redirect(`/hunt/${shortName}/${pkg.card_number}`);
|
||||
});
|
||||
|
||||
// ─── Admin clear hint ─────────────────────────────────
|
||||
router.post('/:shortName/:code/hint/delete', requireAuth, (req, res) => {
|
||||
if (!req.session.isAdmin) {
|
||||
return res.status(403).render('error', { title: 'Forbidden', message: 'Admin access required.' });
|
||||
}
|
||||
const { shortName, code } = req.params;
|
||||
const pkg = Packages.findByHuntAndCode(shortName, code);
|
||||
if (!pkg) {
|
||||
return res.status(404).render('error', { title: 'Not Found', message: 'Package not found.' });
|
||||
}
|
||||
Packages.clearHint(pkg.id);
|
||||
res.redirect(`/hunt/${shortName}/${pkg.card_number}`);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user