QOL improvements
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 28s

This commit is contained in:
2026-02-28 01:14:50 -05:00
parent 79ee7064a8
commit bdb6d5ee25
11 changed files with 264 additions and 4 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
});
});

View File

@@ -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;