Peacemanguy/LLMChoice
0
1const express = require('express');2const bodyParser = require('body-parser');3const fs = require('fs');4const path = require('path');5const crypto = require('crypto');6const session = require('express-session');7const moment = require('moment');8const archiver = require('./leaderboard_archiver');9 10// Constants11const app = express();12const PORT = process.env.ADMIN_PORT || 6969;13const DATA_FILE = path.join(__dirname, 'data', 'data.json');14const IP_FILE = path.join(__dirname, 'data', 'ips.json');15const CATEGORIES = ["6gb", "12gb", "16gb", "24gb", "48gb", "72gb", "96gb"];16 17// Admin credentials - in a real app, store these securely18const ADMIN_USER = 'admin';19const ADMIN_PASS = 'secure_password123'; // Change this to a strong password20 21// Middleware22app.use(bodyParser.urlencoded({ extended: true }));23app.use(bodyParser.json());24app.use(session({25 secret: crypto.randomBytes(32).toString('hex'),26 resave: false,27 saveUninitialized: false,28 cookie: {29 secure: false, // Set to true if using HTTPS30 httpOnly: true,31 maxAge: 3600000 // 1 hour32 }33}));34 35// --- Authentication ---36const authenticate = (req, res, next) => {37 if (req.session && req.session.authenticated) {38 return next();39 }40 res.redirect('/admin');41};42 43// --- Data Handling ---44function readJson(file, fallback) {45 try {46 if (fs.existsSync(file)) {47 return JSON.parse(fs.readFileSync(file));48 }49 return fallback;50 }51 catch { return fallback; }52}53 54function writeJson(file, obj) {55 fs.writeFileSync(file, JSON.stringify(obj, null, 2));56}57 58// --- Routes ---59 60// Simple root message61app.get('/', (req, res) => {62 res.send('Admin Server is running. Access /admin for the interface.');63});64 65// Admin Login Page66app.get('/admin', (req, res) => {67 if (req.session && req.session.authenticated) {68 return res.redirect('/admin/dashboard');69 }70 71 res.send(`72 <!DOCTYPE html>73 <html>74 <head>75 <title>Poll Admin - Login</title>76 <style>77 body { font-family: Arial, sans-serif; margin: 0; padding: 20px; line-height: 1.6; }78 .container { max-width: 500px; margin: 50px auto; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }79 h1 { color: #333; }80 label { display: block; margin-bottom: 5px; }81 input[type="text"], input[type="password"] { width: 100%; padding: 8px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 3px; }82 button { background: #4CAF50; color: white; border: none; padding: 10px 15px; border-radius: 3px; cursor: pointer; }83 button:hover { background: #45a049; }84 .error { color: red; margin-bottom: 15px; }85 </style>86 </head>87 <body>88 <div class="container">89 <h1>Poll Admin Login</h1>90 ${req.query.error ? '<p class="error">Invalid username or password</p>' : ''}91 <form action="/admin/login" method="POST">92 <label for="username">Username:</label>93 <input type="text" id="username" name="username" required>94 95 <label for="password">Password:</label>96 <input type="password" id="password" name="password" required>97 98 <button type="submit">Login</button>99 </form>100 </div>101 </body>102 </html>103 `);104});105 106// Admin Login Handler107app.post('/admin/login', (req, res) => {108 const { username, password } = req.body;109 110 if (username === ADMIN_USER && password === ADMIN_PASS) {111 req.session.authenticated = true;112 req.session.username = username;113 res.redirect('/admin/dashboard');114 } else {115 res.redirect('/admin?error=1');116 }117});118 119// Admin Logout120app.get('/admin/logout', (req, res) => {121 req.session.destroy();122 res.redirect('/admin');123});124 125// Admin Dashboard (Protected)126app.get('/admin/dashboard', authenticate, (req, res) => {127 const data = readJson(DATA_FILE, {});128 129 let categoriesHtml = '';130 131 CATEGORIES.forEach(category => {132 const entries = data[category] || [];133 const sortedEntries = [...entries].sort((a, b) => b.votes - a.votes);134 135 let tableRows = sortedEntries.map((entry) => `136 <tr>137 <td>${escapeHtml(entry.id)}</td>138 <td>${escapeHtml(entry.name)}</td>139 <td>${entry.votes}</td>140 <td>141 <a href="/admin/edit/${category}/${entry.id}" class="btn btn-edit">Edit</a>142 <form action="/admin/delete/${category}/${entry.id}" method="POST" style="display:inline;">143 <button type="submit" class="btn btn-delete" onclick="return confirm('Are you sure you want to delete this entry?')">Delete</button>144 </form>145 </td>146 </tr>147 `).join('');148 149 categoriesHtml += `150 <div class="category-section">151 <h2>${category} Category</h2>152 ${sortedEntries.length > 0 ? `153 <table>154 <thead>155 <tr>156 <th>ID</th>157 <th>Name</th>158 <th>Votes</th>159 <th>Actions</th>160 </tr>161 </thead>162 <tbody>163 ${tableRows}164 </tbody>165 </table>166 ` : '<p>No entries in this category.</p>'}167 </div>168 `;169 });170 171 res.send(`172 <!DOCTYPE html>173 <html>174 <head>175 <title>Poll Admin Dashboard</title>176 <style>177 body { font-family: Arial, sans-serif; margin: 0; padding: 20px; line-height: 1.6; }178 .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }179 h1 { color: #333; margin: 0; }180 table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }181 table, th, td { border: 1px solid #ddd; }182 th { background-color: #f2f2f2; padding: 10px; text-align: left; }183 td { padding: 10px; }184 .category-section { margin-bottom: 30px; }185 .btn { display: inline-block; padding: 5px 10px; margin-right: 5px; text-decoration: none; border-radius: 3px; color: white; border: none; cursor: pointer; }186 .btn-edit { background-color: #2196F3; }187 .btn-delete { background-color: #f44336; }188 .logout { text-decoration: none; color: #f44336; }189 .nav-links { display: flex; gap: 15px; align-items: center; }190 .nav-link { text-decoration: none; color: #2196F3; }191 </style>192 </head>193 <body>194 <div class="header">195 <h1>Poll Admin Dashboard</h1>196 <div class="nav-links">197 <a href="/admin/archives" class="nav-link">View Archives</a>198 <a href="/admin/logout" class="logout">Logout</a>199 </div>200 </div>201 202 ${categoriesHtml}203 </body>204 </html>205 `);206});207 208// Edit Entry Form209app.get('/admin/edit/:category/:id', authenticate, (req, res) => {210 const { category, id } = req.params;211 const data = readJson(DATA_FILE, {});212 213 if (!CATEGORIES.includes(category)) {214 return res.status(400).send('Invalid category');215 }216 217 const entries = data[category] || [];218 const entry = entries.find(e => e.id === id);219 220 if (!entry) {221 return res.status(404).send('Entry not found');222 }223 224 res.send(`225 <!DOCTYPE html>226 <html>227 <head>228 <title>Edit Entry</title>229 <style>230 body { font-family: Arial, sans-serif; margin: 0; padding: 20px; line-height: 1.6; }231 .container { max-width: 600px; margin: 0 auto; }232 h1 { color: #333; }233 label { display: block; margin-bottom: 5px; }234 input[type="text"], input[type="number"] { width: 100%; padding: 8px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 3px; }235 .buttons { margin-top: 20px; }236 button { padding: 8px 15px; margin-right: 10px; border: none; border-radius: 3px; cursor: pointer; }237 .save { background-color: #4CAF50; color: white; }238 .cancel { background-color: #f44336; color: white; }239 </style>240 </head>241 <body>242 <div class="container">243 <h1>Edit Entry</h1>244 <form action="/admin/edit/${category}/${id}" method="POST">245 <label for="name">Name:</label>246 <input type="text" id="name" name="name" value="${escapeHtml(entry.name)}" required>247 248 <label for="votes">Votes:</label>249 <input type="number" id="votes" name="votes" value="${entry.votes}" min="0" required>250 251 <div class="buttons">252 <button type="submit" class="save">Save Changes</button>253 <a href="/admin/dashboard"><button type="button" class="cancel">Cancel</button></a>254 </div>255 </form>256 </div>257 </body>258 </html>259 `);260});261 262// Update Entry263app.post('/admin/edit/:category/:id', authenticate, (req, res) => {264 const { category, id } = req.params;265 const { name, votes } = req.body;266 const data = readJson(DATA_FILE, {});267 268 if (!CATEGORIES.includes(category)) {269 return res.status(400).send('Invalid category');270 }271 272 const entries = data[category] || [];273 const entryIndex = entries.findIndex(e => e.id === id);274 275 if (entryIndex === -1) {276 return res.status(404).send('Entry not found');277 }278 279 // Update entry280 entries[entryIndex].name = name.trim();281 entries[entryIndex].votes = parseInt(votes, 10);282 283 // Save data284 writeJson(DATA_FILE, data);285 console.log(`Updated entry: ${category}/${id}`);286 287 res.redirect('/admin/dashboard');288});289 290// Delete Entry291app.post('/admin/delete/:category/:id', authenticate, (req, res) => {292 const { category, id } = req.params;293 const data = readJson(DATA_FILE, {});294 295 if (!CATEGORIES.includes(category)) {296 return res.status(400).send('Invalid category');297 }298 299 const entries = data[category] || [];300 const entryIndex = entries.findIndex(e => e.id === id);301 302 if (entryIndex === -1) {303 return res.status(404).send('Entry not found');304 }305 306 // Remove entry307 entries.splice(entryIndex, 1);308 309 // Save data310 writeJson(DATA_FILE, data);311 console.log(`Deleted entry: ${category}/${id}`);312 313 // Also clean up any IP votes for this entry314 const ips = readJson(IP_FILE, {});315 let ipChanged = false;316 317 // Check each IP entry318 Object.keys(ips).forEach(ipKey => {319 if (typeof ips[ipKey] === 'object' && ips[ipKey][category] === id) {320 delete ips[ipKey][category];321 ipChanged = true;322 }323 });324 325 if (ipChanged) {326 writeJson(IP_FILE, ips);327 console.log('Updated IP tracking file after entry deletion');328 }329 330 res.redirect('/admin/dashboard');331});332 333// Archives Dashboard334app.get('/admin/archives', authenticate, (req, res) => {335 const archivedWeeks = archiver.getArchivedWeeks();336 337 let archivesHtml = '';338 339 if (archivedWeeks.length === 0) {340 archivesHtml = '<p>No archived data available yet.</p>';341 } else {342 let tableRows = archivedWeeks.map(weekId => {343 const archive = archiver.getArchivedWeek(weekId);344 if (!archive) return '';345 346 return `347 <tr>348 <td>${escapeHtml(archive.weekId)}</td>349 <td>${escapeHtml(archive.startDate)}</td>350 <td>${escapeHtml(archive.endDate)}</td>351 <td>${new Date(archive.archivedAt).toLocaleString()}</td>352 <td>353 <a href="/admin/archives/week/${archive.weekId}" class="btn btn-edit">View</a>354 </td>355 </tr>356 `;357 }).join('');358 359 archivesHtml = `360 <h2>Archived Leaderboards</h2>361 <div class="archive-search">362 <h3>Search Archives by Date Range</h3>363 <form action="/admin/archives/search" method="GET">364 <div class="form-group">365 <label for="startDate">Start Date:</label>366 <input type="date" id="startDate" name="startDate" required>367 </div>368 <div class="form-group">369 <label for="endDate">End Date:</label>370 <input type="date" id="endDate" name="endDate" required>371 </div>372 <button type="submit" class="btn btn-edit">Search</button>373 </form>374 </div>375 376 <h3>All Archived Weeks</h3>377 <table>378 <thead>379 <tr>380 <th>Week ID</th>381 <th>Start Date</th>382 <th>End Date</th>383 <th>Archived At</th>384 <th>Actions</th>385 </tr>386 </thead>387 <tbody>388 ${tableRows}389 </tbody>390 </table>391 392 <div class="archive-actions">393 <form action="/admin/archives/create" method="POST" onsubmit="return confirm('Are you sure you want to archive the current leaderboard data?')">394 <button type="submit" class="btn btn-edit">Archive Current Week</button>395 </form>396 </div>397 `;398 }399 400 res.send(`401 <!DOCTYPE html>402 <html>403 <head>404 <title>Archived Leaderboards</title>405 <style>406 body { font-family: Arial, sans-serif; margin: 0; padding: 20px; line-height: 1.6; }407 .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }408 h1 { color: #333; margin: 0; }409 h2 { color: #333; margin-top: 30px; }410 h3 { color: #555; margin-top: 20px; }411 table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }412 table, th, td { border: 1px solid #ddd; }413 th { background-color: #f2f2f2; padding: 10px; text-align: left; }414 td { padding: 10px; }415 .btn { display: inline-block; padding: 5px 10px; margin-right: 5px; text-decoration: none; border-radius: 3px; color: white; border: none; cursor: pointer; }416 .btn-edit { background-color: #2196F3; }417 .btn-delete { background-color: #f44336; }418 .logout { text-decoration: none; color: #f44336; }419 .nav-links { display: flex; gap: 15px; align-items: center; }420 .nav-link { text-decoration: none; color: #2196F3; }421 .archive-search { margin: 20px 0; padding: 15px; background-color: #f9f9f9; border-radius: 5px; }422 .form-group { margin-bottom: 15px; }423 .form-group label { display: block; margin-bottom: 5px; }424 .form-group input { padding: 8px; width: 200px; }425 .archive-actions { margin-top: 20px; }426 </style>427 </head>428 <body>429 <div class="header">430 <h1>Archived Leaderboards</h1>431 <div class="nav-links">432 <a href="/admin/dashboard" class="nav-link">Back to Dashboard</a>433 <a href="/admin/logout" class="logout">Logout</a>434 </div>435 </div>436 437 ${archivesHtml}438 </body>439 </html>440 `);441});442 443// View specific archived week444app.get('/admin/archives/week/:weekId', authenticate, (req, res) => {445 const { weekId } = req.params;446 const archive = archiver.getArchivedWeek(weekId);447 448 if (!archive) {449 return res.status(404).send('Archive not found');450 }451 452 let categoriesHtml = '';453 454 CATEGORIES.forEach(category => {455 const entries = archive.data[category] || [];456 const sortedEntries = [...entries].sort((a, b) => b.votes - a.votes);457 458 let tableRows = sortedEntries.map((entry) => `459 <tr>460 <td>${escapeHtml(entry.id)}</td>461 <td>${escapeHtml(entry.name)}</td>462 <td>${entry.votes}</td>463 </tr>464 `).join('');465 466 categoriesHtml += `467 <div class="category-section">468 <h2>${category} Category</h2>469 ${sortedEntries.length > 0 ? `470 <table>471 <thead>472 <tr>473 <th>ID</th>474 <th>Name</th>475 <th>Votes</th>476 </tr>477 </thead>478 <tbody>479 ${tableRows}480 </tbody>481 </table>482 ` : '<p>No entries in this category.</p>'}483 </div>484 `;485 });486 487 res.send(`488 <!DOCTYPE html>489 <html>490 <head>491 <title>Archived Week: ${weekId}</title>492 <style>493 body { font-family: Arial, sans-serif; margin: 0; padding: 20px; line-height: 1.6; }494 .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }495 h1 { color: #333; margin: 0; }496 h2 { color: #333; margin-top: 30px; }497 table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }498 table, th, td { border: 1px solid #ddd; }499 th { background-color: #f2f2f2; padding: 10px; text-align: left; }500 td { padding: 10px; }501 .category-section { margin-bottom: 30px; }502 .btn { display: inline-block; padding: 5px 10px; margin-right: 5px; text-decoration: none; border-radius: 3px; color: white; border: none; cursor: pointer; }503 .btn-edit { background-color: #2196F3; }504 .nav-links { display: flex; gap: 15px; align-items: center; }505 .nav-link { text-decoration: none; color: #2196F3; }506 .archive-meta { background-color: #f9f9f9; padding: 15px; border-radius: 5px; margin-bottom: 20px; }507 .archive-meta p { margin: 5px 0; }508 </style>509 </head>510 <body>511 <div class="header">512 <h1>Archived Week: ${weekId}</h1>513 <div class="nav-links">514 <a href="/admin/archives" class="nav-link">Back to Archives</a>515 <a href="/admin/dashboard" class="nav-link">Back to Dashboard</a>516 <a href="/admin/logout" class="logout">Logout</a>517 </div>518 </div>519 520 <div class="archive-meta">521 <p><strong>Week ID:</strong> ${archive.weekId}</p>522 <p><strong>Start Date:</strong> ${archive.startDate}</p>523 <p><strong>End Date:</strong> ${archive.endDate}</p>524 <p><strong>Archived At:</strong> ${new Date(archive.archivedAt).toLocaleString()}</p>525 </div>526 527 ${categoriesHtml}528 </body>529 </html>530 `);531});532 533// Search archives by date range534app.get('/admin/archives/search', authenticate, (req, res) => {535 const { startDate, endDate } = req.query;536 537 if (!startDate || !endDate) {538 return res.redirect('/admin/archives');539 }540 541 try {542 const archives = archiver.getArchivedRange(startDate, endDate);543 544 let resultsHtml = '';545 546 if (archives.length === 0) {547 resultsHtml = '<p>No archives found for the specified date range.</p>';548 } else {549 let tableRows = archives.map(archive => `550 <tr>551 <td>${escapeHtml(archive.weekId)}</td>552 <td>${escapeHtml(archive.startDate)}</td>553 <td>${escapeHtml(archive.endDate)}</td>554 <td>${new Date(archive.archivedAt).toLocaleString()}</td>555 <td>556 <a href="/admin/archives/week/${archive.weekId}" class="btn btn-edit">View</a>557 </td>558 </tr>559 `).join('');560 561 resultsHtml = `562 <h3>Search Results</h3>563 <p>Found ${archives.length} archive(s) between ${startDate} and ${endDate}</p>564 <table>565 <thead>566 <tr>567 <th>Week ID</th>568 <th>Start Date</th>569 <th>End Date</th>570 <th>Archived At</th>571 <th>Actions</th>572 </tr>573 </thead>574 <tbody>575 ${tableRows}576 </tbody>577 </table>578 `;579 }580 581 res.send(`582 <!DOCTYPE html>583 <html>584 <head>585 <title>Archive Search Results</title>586 <style>587 body { font-family: Arial, sans-serif; margin: 0; padding: 20px; line-height: 1.6; }588 .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }589 h1 { color: #333; margin: 0; }590 h3 { color: #555; margin-top: 20px; }591 table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }592 table, th, td { border: 1px solid #ddd; }593 th { background-color: #f2f2f2; padding: 10px; text-align: left; }594 td { padding: 10px; }595 .btn { display: inline-block; padding: 5px 10px; margin-right: 5px; text-decoration: none; border-radius: 3px; color: white; border: none; cursor: pointer; }596 .btn-edit { background-color: #2196F3; }597 .nav-links { display: flex; gap: 15px; align-items: center; }598 .nav-link { text-decoration: none; color: #2196F3; }599 .search-form { margin: 20px 0; padding: 15px; background-color: #f9f9f9; border-radius: 5px; }600 .form-group { margin-bottom: 15px; }601 .form-group label { display: block; margin-bottom: 5px; }602 .form-group input { padding: 8px; width: 200px; }603 </style>604 </head>605 <body>606 <div class="header">607 <h1>Archive Search Results</h1>608 <div class="nav-links">609 <a href="/admin/archives" class="nav-link">Back to Archives</a>610 <a href="/admin/dashboard" class="nav-link">Back to Dashboard</a>611 <a href="/admin/logout" class="logout">Logout</a>612 </div>613 </div>614 615 <div class="search-form">616 <h3>Search Archives by Date Range</h3>617 <form action="/admin/archives/search" method="GET">618 <div class="form-group">619 <label for="startDate">Start Date:</label>620 <input type="date" id="startDate" name="startDate" value="${startDate}" required>621 </div>622 <div class="form-group">623 <label for="endDate">End Date:</label>624 <input type="date" id="endDate" name="endDate" value="${endDate}" required>625 </div>626 <button type="submit" class="btn btn-edit">Search</button>627 </form>628 </div>629 630 ${resultsHtml}631 </body>632 </html>633 `);634 } catch (error) {635 console.error('Error searching archives:', error);636 res.redirect('/admin/archives?error=1');637 }638});639 640// Manually create an archive641app.post('/admin/archives/create', authenticate, (req, res) => {642 try {643 const weekId = archiver.archiveCurrentWeek();644 // Reset votes after archiving645 archiver.resetLeaderboard();646 res.redirect(`/admin/archives/week/${weekId}`);647 } catch (error) {648 console.error('Error creating archive:', error);649 res.redirect('/admin/archives?error=1');650 }651});652 653// Helper function to escape HTML (prevent XSS)654function escapeHtml(unsafe) {655 if (typeof unsafe !== 'string') return '';656 return unsafe657 .replace(/&/g, "&")658 .replace(/</g, "<")659 .replace(/>/g, ">")660 .replace(/"/g, """)661 .replace(/'/g, "'");662}663 664// Start Server665app.listen(PORT, () => {666 console.log(`Admin server listening on http://localhost:${PORT}`);667});