Fix missing DB table, rules column, GM endpoints, and logout
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user