From 43d0becdf6341a2a724152d8670148790bc924a3 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 1 Mar 2026 18:07:31 +0100 Subject: [PATCH] Fix missing DB table, rules column, GM endpoints, and logout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add rules table to createTables() with auto-seed from rules-database.json - Fix page_num → page column mapping in rulesRoutes.js - Fix INSERT column names in rulesHelpers.create() and stagingHelpers - Add POST /api/sessions/logout endpoint - Add GM endpoints: set-xp, set-xp-spent, set-rp, set-renown, add-or-update, reset-password, DELETE gm/delete/:name - All GM endpoints require x-gm-secret header (403 otherwise) Co-Authored-By: Claude Sonnet 4.6 --- database/mariadb.js | 63 +++++++++++-- database/routes/playerRoutes.js | 147 +++++++++++++++++++++++++++++++ database/routes/rulesRoutes.js | 30 +++---- database/routes/sessionRoutes.js | 14 +++ 4 files changed, 233 insertions(+), 21 deletions(-) diff --git a/database/mariadb.js b/database/mariadb.js index 0303863..5654132 100644 --- a/database/mariadb.js +++ b/database/mariadb.js @@ -73,15 +73,34 @@ const createTables = async () => { ) `); + // Rules table + await connection.execute(` + CREATE TABLE IF NOT EXISTS rules ( + id INT AUTO_INCREMENT PRIMARY KEY, + rule_id VARCHAR(255) UNIQUE, + title VARCHAR(500), + content TEXT, + page INT, + source VARCHAR(255), + source_abbr VARCHAR(50), + category VARCHAR(100), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + // Create indexes await connection.execute(`CREATE INDEX IF NOT EXISTS idx_players_name ON players(name)`); await connection.execute(`CREATE INDEX IF NOT EXISTS idx_sessions_session_id ON sessions(session_id)`); await connection.execute(`CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at)`); await connection.execute(`CREATE INDEX IF NOT EXISTS idx_rules_staging_category ON rules_staging(category)`); + await connection.execute(`CREATE INDEX IF NOT EXISTS idx_rules_category ON rules(category)`); connection.release(); console.log('MariaDB tables created successfully'); logToFile('MariaDB: Tables created successfully'); + + // Seed rules from JSON if table is empty + await seedRulesIfEmpty(); } catch (error) { console.error('Error creating MariaDB tables:', error); logToFile('MariaDB: Error creating tables', error); @@ -89,6 +108,38 @@ const createTables = async () => { } }; +// Seed rules table from rules-database.json if empty +const seedRulesIfEmpty = async () => { + try { + const [countRows] = await pool.execute('SELECT COUNT(*) AS cnt FROM rules'); + if (countRows[0].cnt > 0) return; + + const dbJsonPath = path.join(__dirname, 'rules', 'rules-database.json'); + if (!fs.existsSync(dbJsonPath)) { + logToFile('MariaDB: rules-database.json not found, skipping seed'); + return; + } + + const data = JSON.parse(fs.readFileSync(dbJsonPath, 'utf8')); + const rules = data.rules || []; + if (!rules.length) return; + + const connection = await pool.getConnection(); + for (const rule of rules) { + await connection.execute( + 'INSERT IGNORE INTO rules (rule_id, title, content, page, source, source_abbr, category) VALUES (?, ?, ?, ?, ?, ?, ?)', + [rule.id || null, rule.title || '', rule.content || '', rule.page || null, rule.source || null, rule.sourceAbbr || null, rule.category || null] + ); + } + connection.release(); + console.log(`MariaDB: Seeded ${rules.length} rules from rules-database.json`); + logToFile(`MariaDB: Seeded ${rules.length} rules from rules-database.json`); + } catch (error) { + console.error('Error seeding rules:', error); + logToFile('MariaDB: Error seeding rules', error); + } +}; + // Player helpers const playerHelpers = { getAll: async () => { @@ -274,8 +325,8 @@ const rulesHelpers = { create: async (rule) => { try { const [result] = await pool.execute( - 'INSERT INTO rules (title, content, source, page_num, rulebook, created_at) VALUES (?, ?, ?, ?, ?, NOW())', - [rule.title, rule.content, rule.source, rule.page_num, rule.rulebook] + 'INSERT INTO rules (rule_id, title, content, page, source, source_abbr, category) VALUES (?, ?, ?, ?, ?, ?, ?)', + [rule.rule_id || null, rule.title, rule.content, rule.page || null, rule.source || null, rule.source_abbr || null, rule.category || null] ); return result.insertId; } catch (error) { @@ -310,8 +361,8 @@ const stagingHelpers = { create: async (rule) => { try { const [result] = await pool.execute( - 'INSERT INTO rules_staging (title, content, source, page_num, rulebook, created_at) VALUES (?, ?, ?, ?, ?, NOW())', - [rule.title, rule.content, rule.source, rule.page_num, rule.rulebook] + 'INSERT INTO rules_staging (title, content, category, page, original_json) VALUES (?, ?, ?, ?, ?)', + [rule.title, rule.content, rule.category || null, rule.page || null, rule.original_json || null] ); return result.insertId; } catch (error) { @@ -347,8 +398,8 @@ const stagingHelpers = { // Insert into rules await connection.execute( - 'INSERT INTO rules (title, content, source, page_num, rulebook, created_at) VALUES (?, ?, ?, ?, ?, NOW())', - [rule.title, rule.content, rule.source, rule.page_num, rule.rulebook] + 'INSERT INTO rules (rule_id, title, content, page, source, source_abbr, category) VALUES (?, ?, ?, ?, ?, ?, ?)', + [rule.rule_id || null, rule.title, rule.content, rule.page || null, rule.source || null, rule.source_abbr || null, rule.category || null] ); // Delete from staging diff --git a/database/routes/playerRoutes.js b/database/routes/playerRoutes.js index b1aace2..b0b13a0 100644 --- a/database/routes/playerRoutes.js +++ b/database/routes/playerRoutes.js @@ -153,6 +153,153 @@ router.post('/', async (req, res) => { } }); +// --- GM Endpoints (require x-gm-secret header) --- + +function requireGM(req, res) { + if (req.headers['x-gm-secret'] !== 'bongo') { + res.status(403).json({ error: 'Unauthorized' }); + return false; + } + return true; +} + +// Helper: update a field inside tab_info JSON +async function updateTabInfo(name, fields) { + const player = await playerHelpers.getByName(name); + if (!player) return null; + const tabInfo = { ...(player.tabInfo || {}), ...fields }; + await playerHelpers.update(name, { + rollerInfo: player.rollerInfo, + shopInfo: player.shopInfo, + tabInfo, + pw: player.pw, + pwHash: player.pwHash, + }); + return true; +} + +// Set XP for a player +router.post('/gm/set-xp', async (req, res) => { + if (!requireGM(req, res)) return; + try { + const { playerName, xp } = req.body; + const ok = await updateTabInfo(playerName, { xp: parseInt(xp) || 0 }); + if (!ok) return res.status(404).json({ error: 'Player not found' }); + logToFile('API: GM set XP', playerName, xp); + res.json({ success: true }); + } catch (e) { + logToFile('API: GM set XP error', e); + res.status(500).json({ error: String(e) }); + } +}); + +// Set XP Spent for a player +router.post('/gm/set-xp-spent', async (req, res) => { + if (!requireGM(req, res)) return; + try { + const { playerName, xpSpent } = req.body; + const ok = await updateTabInfo(playerName, { xpSpent: parseInt(xpSpent) || 0 }); + if (!ok) return res.status(404).json({ error: 'Player not found' }); + logToFile('API: GM set XP spent', playerName, xpSpent); + res.json({ success: true }); + } catch (e) { + logToFile('API: GM set XP spent error', e); + res.status(500).json({ error: String(e) }); + } +}); + +// Set Requisition Points for a player +router.post('/gm/set-rp', async (req, res) => { + if (!requireGM(req, res)) return; + try { + const { playerName, requisitionPoints } = req.body; + const ok = await updateTabInfo(playerName, { rp: parseInt(requisitionPoints) || 0 }); + if (!ok) return res.status(404).json({ error: 'Player not found' }); + logToFile('API: GM set RP', playerName, requisitionPoints); + res.json({ success: true }); + } catch (e) { + logToFile('API: GM set RP error', e); + res.status(500).json({ error: String(e) }); + } +}); + +// Set Renown for a player +router.post('/gm/set-renown', async (req, res) => { + if (!requireGM(req, res)) return; + try { + const { playerName, renown } = req.body; + const ok = await updateTabInfo(playerName, { renown }); + if (!ok) return res.status(404).json({ error: 'Player not found' }); + logToFile('API: GM set renown', playerName, renown); + res.json({ success: true }); + } catch (e) { + logToFile('API: GM set renown error', e); + res.status(500).json({ error: String(e) }); + } +}); + +// Add or update a player (create if not exists, set RP) +router.post('/gm/add-or-update', async (req, res) => { + if (!requireGM(req, res)) return; + try { + const { name, requisitionPoints, password } = req.body; + if (!name) return res.status(400).json({ error: 'Name required' }); + + let player = await playerHelpers.getByName(name); + if (!player) { + await playerHelpers.create({ name, pw: password || '1234', tabInfo: { rp: parseInt(requisitionPoints) || 0 } }); + logToFile('API: GM added player', name); + } else { + await updateTabInfo(name, { rp: parseInt(requisitionPoints) || 0 }); + if (password) { + await playerHelpers.update(name, { rollerInfo: player.rollerInfo, shopInfo: player.shopInfo, tabInfo: player.tabInfo, pw: password, pwHash: '' }); + } + logToFile('API: GM updated player', name); + } + res.json({ success: true }); + } catch (e) { + logToFile('API: GM add-or-update error', e); + res.status(500).json({ error: String(e) }); + } +}); + +// Reset player password +router.post('/gm/reset-password', async (req, res) => { + if (!requireGM(req, res)) return; + try { + const { playerName, newPassword } = req.body; + const player = await playerHelpers.getByName(playerName); + if (!player) return res.status(404).json({ error: 'Player not found' }); + await playerHelpers.update(playerName, { + rollerInfo: player.rollerInfo, + shopInfo: player.shopInfo, + tabInfo: player.tabInfo, + pw: newPassword || '1234', + pwHash: '', + }); + logToFile('API: GM reset password', playerName); + res.json({ success: true }); + } catch (e) { + logToFile('API: GM reset password error', e); + res.status(500).json({ error: String(e) }); + } +}); + +// Delete player (GM) +router.delete('/gm/delete/:name', async (req, res) => { + if (!requireGM(req, res)) return; + try { + const { name } = req.params; + const success = await playerHelpers.delete(name); + if (!success) return res.status(404).json({ error: 'Player not found or could not be deleted' }); + logToFile('API: GM deleted player', name); + res.json({ success: true }); + } catch (e) { + logToFile('API: GM delete player error', e); + res.status(500).json({ error: String(e) }); + } +}); + // Delete player router.delete('/:name', async (req, res) => { try { diff --git a/database/routes/rulesRoutes.js b/database/routes/rulesRoutes.js index 5d143d4..9b4adaf 100644 --- a/database/routes/rulesRoutes.js +++ b/database/routes/rulesRoutes.js @@ -37,14 +37,14 @@ async function getAllRules() { try { const rows = await rulesHelpers.getAll(); return rows.map(r => ({ - id: r.id, - rule_id: r.rule_id, - title: cleanTitle(r.title), - content: cleanText(r.content), - page: r.page_num, - source: r.source, - sourceAbbr: r.source_abbr, - category: r.category + id: r.id, + rule_id: r.rule_id, + title: cleanTitle(r.title), + content: cleanText(r.content), + page: r.page, + source: r.source, + sourceAbbr: r.source_abbr, + category: r.category })); } catch (e) { console.error('Failed to read rules from MariaDB:', e); @@ -59,13 +59,13 @@ async function getRuleById(ruleId) { const row = rows.find(r => r.rule_id === ruleId || r.id === ruleId); if (!row) return null; return { - id: row.rule_id || row.id, - title: cleanTitle(row.title), - content: cleanText(row.content), - page: row.page_num, - source: row.source, - sourceAbbr: row.source_abbr, - category: row.category + id: row.rule_id || row.id, + title: cleanTitle(row.title), + content: cleanText(row.content), + page: row.page, + source: row.source, + sourceAbbr: row.source_abbr, + category: row.category }; } catch (e) { console.error('Failed to read rule by id:', e); diff --git a/database/routes/sessionRoutes.js b/database/routes/sessionRoutes.js index 274d082..354dcec 100644 --- a/database/routes/sessionRoutes.js +++ b/database/routes/sessionRoutes.js @@ -33,5 +33,19 @@ router.post('/validate', async (req, res) => { } }); +// Logout endpoint +router.post('/logout', async (req, res) => { + try { + const { sessionId } = req.body; + if (sessionId) { + logToFile('SESSION: Logout', sessionId); + } + res.json({ success: true }); + } catch (error) { + logToFile('SESSION: Logout error', error); + res.status(500).json({ error: String(error) }); + } +}); + console.log('Session routes registered (simple validation)'); module.exports = router;