Files
loot-hunt/src/routes/hunts.js
Mike Johnston 6f7ccb6409
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 29s
fix already approved org application
2026-03-20 22:15:03 -04:00

221 lines
9.7 KiB
JavaScript

const express = require('express');
const router = express.Router();
const { requireAuth } = require('../middleware/auth');
const { Hunts, Packages, Scans, Users, OrganizerApplications } = require('../models');
// ─── Hunt profile ─────────────────────────────────────────
router.get('/hunt/:shortName', (req, res) => {
const hunt = Hunts.findByShortName(req.params.shortName);
if (!hunt) {
return res.status(404).render('error', { title: 'Not Found', message: 'Hunt not found.' });
}
// Block access if hidden and not started (unless admin/organizer)
if (Hunts.isHidden(hunt) && !(req.session && (req.session.isAdmin || req.session.isOrganizer))) {
return res.status(404).render('error', { title: 'Not Found', message: 'Hunt not found.' });
}
const packages = Packages.getByHunt(hunt.id);
const isExpired = Hunts.isExpired(hunt);
const hasStarted = Hunts.hasStarted(hunt);
res.render('hunt/profile', { title: hunt.name, hunt, packages, isExpired, hasStarted });
});
// ─── Hunt leaderboard ─────────────────────────────────────
router.get('/hunt/:shortName/leaderboard', (req, res) => {
const hunt = Hunts.findByShortName(req.params.shortName);
if (!hunt) {
return res.status(404).render('error', { title: 'Not Found', message: 'Hunt not found.' });
}
if (Hunts.isHidden(hunt) && !(req.session && (req.session.isAdmin || req.session.isOrganizer))) {
return res.status(404).render('error', { title: 'Not Found', message: 'Hunt not found.' });
}
const perPage = 25;
const page = Math.max(1, parseInt(req.query.page) || 1);
const totalCount = Hunts.getLeaderboardCount(hunt.id);
const totalPages = Math.max(1, Math.ceil(totalCount / perPage));
const leaderboard = Hunts.getLeaderboard(hunt.id, perPage, (page - 1) * perPage);
res.render('hunt/leaderboard', { title: `${hunt.name} - Leaderboard`, hunt, leaderboard, page, totalPages, offset: (page - 1) * perPage });
});
// ─── Global leaderboard ──────────────────────────────────
router.get('/leaderboard', (req, res) => {
const perPage = 25;
const page = Math.max(1, parseInt(req.query.page) || 1);
const totalCount = Scans.getGlobalLeaderboardCount();
const totalPages = Math.max(1, Math.ceil(totalCount / perPage));
const leaderboard = Scans.getGlobalLeaderboard(perPage, (page - 1) * perPage);
res.render('leaderboard/global', { title: 'Global Leaderboard', leaderboard, page, totalPages, offset: (page - 1) * perPage });
});
// ─── Package profile (by card number — no secret code exposed) ────
router.get('/hunt/:shortName/:cardNumber', (req, res) => {
const { shortName, cardNumber } = req.params;
if (!/^\d+$/.test(cardNumber)) {
return res.status(404).render('error', { title: 'Not Found', message: 'Package not found.' });
}
const pkg = Packages.findByHuntAndCardNumber(shortName, cardNumber);
if (!pkg) {
return res.status(404).render('error', { title: 'Not Found', message: 'Package not found.' });
}
const fullPkg = Packages.getProfile(pkg.id);
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}`,
pkg: fullPkg,
scanHistory,
isFirstScanner,
isLastScanner,
isAdmin,
packages_total: pkg.package_count
});
});
// ─── User profile ─────────────────────────────────────────
router.get('/player/:username', (req, res) => {
const user = Users.findByUsername(req.params.username);
if (!user) {
return res.status(404).render('error', { title: 'Not Found', message: 'Player not found.' });
}
const profile = Users.getProfile(user.id);
const recentScans = Users.getRecentScans(user.id);
const huntBreakdown = Users.getHuntBreakdown(user.id);
const rank = Users.getRank(user.id);
const totalPlayers = Users.getTotalPlayerCount();
const isOwnProfile = req.session && req.session.userId === user.id;
const pendingApplication = isOwnProfile ? OrganizerApplications.findByUser(user.id) : null;
res.render('player/profile', {
title: `${user.username}'s Profile`,
profile,
recentScans,
huntBreakdown,
rank,
totalPlayers,
isOwnProfile,
pendingApplication
});
});
// ─── Change password (own profile) ────────────────────────
router.post('/player/:username/password', requireAuth, (req, res) => {
const user = Users.findByUsername(req.params.username);
if (!user || user.id !== req.session.userId) {
return res.status(403).render('error', { title: 'Forbidden', message: 'You can only change your own password.' });
}
const { current_password, new_password, new_password_confirm } = req.body;
const fullUser = Users.findByUsername(user.username);
if (!Users.verifyPassword(fullUser, current_password)) {
req.session.flash = { type: 'danger', message: 'Current password is incorrect.' };
return res.redirect(`/player/${user.username}`);
}
if (!new_password || new_password.length < 6) {
req.session.flash = { type: 'danger', message: 'New password must be at least 6 characters.' };
return res.redirect(`/player/${user.username}`);
}
if (new_password !== new_password_confirm) {
req.session.flash = { type: 'danger', message: 'New passwords do not match.' };
return res.redirect(`/player/${user.username}`);
}
Users.setPassword(user.id, new_password);
req.session.flash = { type: 'success', message: 'Password changed successfully.' };
res.redirect(`/player/${user.username}`);
});
// ─── Update display name ──────────────────────────────────
router.post('/player/:username/display-name', requireAuth, (req, res) => {
const user = Users.findByUsername(req.params.username);
if (!user || user.id !== req.session.userId) {
return res.status(403).render('error', { title: 'Forbidden', message: 'You can only change your own display name.' });
}
const displayName = (req.body.display_name || '').trim();
if (displayName.length < 2 || displayName.length > 32) {
req.session.flash = { type: 'danger', message: 'Display name must be 2-32 characters.' };
return res.redirect(`/player/${user.username}`);
}
Users.setDisplayName(user.id, displayName);
req.session.displayName = displayName;
req.session.flash = { type: 'success', message: 'Display name updated.' };
res.redirect(`/player/${user.username}`);
});
// ─── Apply to become organizer ────────────────────────────
router.get('/apply-organizer', requireAuth, (req, res) => {
const user = Users.findById(req.session.userId);
if (user.is_organizer || user.is_admin) {
return res.render('apply-organizer', { title: 'Organizer Access', pendingApplication: null, alreadyOrganizer: true });
}
const pendingApplication = OrganizerApplications.findByUser(user.id);
res.render('apply-organizer', { title: 'Apply to Become an Organizer', pendingApplication, alreadyOrganizer: false });
});
router.post('/apply-organizer', requireAuth, (req, res) => {
const user = Users.findById(req.session.userId);
if (user.is_organizer || user.is_admin) {
req.session.flash = { type: 'info', message: 'You already have organizer access.' };
return res.redirect(`/player/${user.username}`);
}
if (OrganizerApplications.findByUser(user.id)) {
req.session.flash = { type: 'info', message: 'You already have a pending application.' };
return res.redirect('/apply-organizer');
}
const reason = (req.body.reason || '').trim();
if (!reason || reason.length < 10) {
req.session.flash = { type: 'danger', message: 'Please provide a reason (at least 10 characters).' };
return res.redirect('/apply-organizer');
}
if (reason.length > 1000) {
req.session.flash = { type: 'danger', message: 'Reason is too long (max 1000 characters).' };
return res.redirect('/apply-organizer');
}
OrganizerApplications.submit(user.id, reason);
req.session.flash = { type: 'success', message: 'Your organizer application has been submitted!' };
res.redirect('/apply-organizer');
});
// ─── Delete own account ───────────────────────────────────
router.post('/player/:username/delete', requireAuth, (req, res) => {
const user = Users.findByUsername(req.params.username);
if (!user || user.id !== req.session.userId) {
return res.status(403).render('error', { title: 'Forbidden', message: 'You can only delete your own account.' });
}
if (user.is_admin) {
req.session.flash = { type: 'danger', message: 'Admin accounts cannot be deleted.' };
return res.redirect(`/player/${user.username}`);
}
Users.deleteUser(user.id);
req.session.destroy(() => {
res.redirect('/');
});
});
// ─── Browse all hunts ─────────────────────────────────────
router.get('/hunts', (req, res) => {
const allHunts = Hunts.getAll();
const hunts = allHunts.filter(h => !Hunts.isHidden(h));
res.render('hunt/list', { title: 'All Hunts', hunts });
});
module.exports = router;