diff --git a/database/mariadb-schema-update.sql b/database/mariadb-schema-update.sql new file mode 100644 index 0000000..82f5e3d --- /dev/null +++ b/database/mariadb-schema-update.sql @@ -0,0 +1,96 @@ +-- Add missing tables to MariaDB schema + +-- Shop items table +CREATE TABLE IF NOT EXISTS shop_items ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + category VARCHAR(100) NOT NULL, + requisition_cost INT NOT NULL DEFAULT 0, + renown_requirement VARCHAR(50) NOT NULL DEFAULT 'None', + item_type VARCHAR(100) NOT NULL, + stats TEXT NOT NULL, + source VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +-- Player inventory table +CREATE TABLE IF NOT EXISTS player_inventory ( + id INT AUTO_INCREMENT PRIMARY KEY, + player_id INT NOT NULL, + item_id INT NOT NULL, + quantity INT NOT NULL DEFAULT 1, + acquired_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + transaction_details TEXT, + FOREIGN KEY (player_id) REFERENCES players (id) ON DELETE CASCADE, + FOREIGN KEY (item_id) REFERENCES shop_items (id) ON DELETE CASCADE, + UNIQUE(player_id, item_id) +); + +-- Transactions table +CREATE TABLE IF NOT EXISTS transactions ( + id INT AUTO_INCREMENT PRIMARY KEY, + player_id INT NOT NULL, + item_id INT NOT NULL, + requisition_cost INT NOT NULL, + previous_rp INT NOT NULL, + new_rp INT NOT NULL, + transaction_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (player_id) REFERENCES players (id) ON DELETE CASCADE, + FOREIGN KEY (item_id) REFERENCES shop_items (id) ON DELETE CASCADE +); + +-- Armour table +CREATE TABLE IF NOT EXISTS armour ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) UNIQUE NOT NULL, + req INT DEFAULT 0, + renown VARCHAR(50) DEFAULT 'None', + category VARCHAR(100), + stats TEXT, + source VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Weapons table +CREATE TABLE IF NOT EXISTS weapons ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) UNIQUE NOT NULL, + req INT DEFAULT 0, + renown VARCHAR(50) DEFAULT 'None', + category VARCHAR(100), + stats TEXT, + source VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Bestiary table +CREATE TABLE IF NOT EXISTS bestiary ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + book VARCHAR(255), + page VARCHAR(50), + pdf VARCHAR(255), + stats TEXT, + profile TEXT, + snippet TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Rules table +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 +); + +-- Add missing columns to players table +ALTER TABLE players +ADD COLUMN IF NOT EXISTS requisition_points INT DEFAULT 0, +ADD COLUMN IF NOT EXISTS renown_level VARCHAR(50) DEFAULT 'None'; diff --git a/database/mariadb.js b/database/mariadb.js new file mode 100644 index 0000000..1c95d6a --- /dev/null +++ b/database/mariadb.js @@ -0,0 +1,408 @@ +const mysql = require('mysql2/promise'); +const fs = require('fs'); +const path = require('path'); + +// Simple DB logger to backend.log +const backendLogPath = path.join(__dirname, 'backend.log'); +function logToFile(...args) { + try { + const msg = `[${new Date().toISOString()}] ` + args.map(a => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ') + '\n'; + fs.appendFileSync(backendLogPath, msg, { encoding: 'utf8' }); + } catch (err) { + console.error('Failed to write backend log', err); + } +} + +// Database configuration +const dbConfig = { + host: 'localhost', + user: 'deathwatch', + password: 'dwroller2025', + database: 'deathwatch', + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 +}; + +// Create connection pool +const pool = mysql.createPool(dbConfig); + +// Create tables +const createTables = async () => { + try { + const connection = await pool.getConnection(); + + // Players table + await connection.execute(` + CREATE TABLE IF NOT EXISTS players ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) UNIQUE NOT NULL, + roller_info JSON DEFAULT ('{}'), + shop_info JSON DEFAULT ('{}'), + tab_info JSON DEFAULT ('{}'), + pw VARCHAR(255) DEFAULT '', + pw_hash VARCHAR(255) DEFAULT '', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) + `); + + // Sessions table + await connection.execute(` + CREATE TABLE IF NOT EXISTS sessions ( + id INT AUTO_INCREMENT PRIMARY KEY, + session_id VARCHAR(255) UNIQUE NOT NULL, + data JSON DEFAULT ('{}'), + expires_at TIMESTAMP NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) + `); + + // Rules staging table + await connection.execute(` + CREATE TABLE IF NOT EXISTS rules_staging ( + id INT AUTO_INCREMENT PRIMARY KEY, + title TEXT, + content TEXT, + category VARCHAR(255), + page VARCHAR(255), + original_json TEXT, + 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)`); + + connection.release(); + console.log('MariaDB tables created successfully'); + logToFile('MariaDB: Tables created successfully'); + } catch (error) { + console.error('Error creating MariaDB tables:', error); + logToFile('MariaDB: Error creating tables', error); + throw error; + } +}; + +// Player helpers +const playerHelpers = { + getAll: async () => { + try { + const [rows] = await pool.execute('SELECT * FROM players ORDER BY name'); + return rows.map(row => ({ + id: row.id, + name: row.name, + rollerInfo: typeof row.roller_info === 'string' ? JSON.parse(row.roller_info) : row.roller_info, + shopInfo: typeof row.shop_info === 'string' ? JSON.parse(row.shop_info) : row.shop_info, + tabInfo: typeof row.tab_info === 'string' ? JSON.parse(row.tab_info) : row.tab_info, + pw: row.pw || '', + pwHash: row.pw_hash || '', + requisitionPoints: row.requisition_points || 0, + renownLevel: row.renown_level || 'None', + createdAt: row.created_at, + updatedAt: row.updated_at, + _id: row.id + })); + } catch (error) { + logToFile('MariaDB: Error getting all players', error); + return []; + } + }, + + getByName: async (name) => { + try { + const [rows] = await pool.execute('SELECT * FROM players WHERE name = ?', [name]); + if (rows.length === 0) return null; + + const row = rows[0]; + return { + id: row.id, + name: row.name, + rollerInfo: typeof row.roller_info === 'string' ? JSON.parse(row.roller_info) : row.roller_info, + shopInfo: typeof row.shop_info === 'string' ? JSON.parse(row.shop_info) : row.shop_info, + tabInfo: typeof row.tab_info === 'string' ? JSON.parse(row.tab_info) : row.tab_info, + pw: row.pw || '', + pwHash: row.pw_hash || '', + requisitionPoints: row.requisition_points || 0, + renownLevel: row.renown_level || 'None', + createdAt: row.created_at, + updatedAt: row.updated_at, + _id: row.id + }; + } catch (error) { + logToFile('MariaDB: Error getting player by name', name, error); + return null; + } + }, + + create: async (playerData) => { + try { + const { name, rollerInfo = {}, shopInfo = {}, tabInfo = {}, pw = '', pwHash = '' } = playerData; + + const [result] = await pool.execute( + 'INSERT INTO players (name, roller_info, shop_info, tab_info, pw, pw_hash) VALUES (?, ?, ?, ?, ?, ?)', + [name, JSON.stringify(rollerInfo), JSON.stringify(shopInfo), JSON.stringify(tabInfo), pw, pwHash] + ); + + logToFile('MariaDB: Created player', name); + return result.insertId; + } catch (error) { + logToFile('MariaDB: Error creating player', playerData.name, error); + return null; + } + }, + + update: async (name, playerData) => { + try { + const { rollerInfo, shopInfo, tabInfo, pw, pwHash } = playerData; + + const [result] = await pool.execute( + 'UPDATE players SET roller_info = ?, shop_info = ?, tab_info = ?, pw = ?, pw_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?', + [ + JSON.stringify(rollerInfo || {}), + JSON.stringify(shopInfo || {}), + JSON.stringify(tabInfo || {}), + pw || '', + pwHash || '', + name + ] + ); + + logToFile('MariaDB: Updated player', name); + return result.affectedRows > 0; + } catch (error) { + logToFile('MariaDB: Error updating player', name, error); + return false; + } + }, + + delete: async (name) => { + try { + const [result] = await pool.execute('DELETE FROM players WHERE name = ?', [name]); + logToFile('MariaDB: Deleted player', name); + return result.affectedRows > 0; + } catch (error) { + logToFile('MariaDB: Error deleting player', name, error); + return false; + } + } +}; + +// Session helpers +const sessionHelpers = { + create: async (sessionId, data = {}, expiresAt = null) => { + try { + const [result] = await pool.execute( + 'INSERT INTO sessions (session_id, data, expires_at) VALUES (?, ?, ?)', + [sessionId, JSON.stringify(data), expiresAt] + ); + return result.insertId; + } catch (error) { + logToFile('MariaDB: Error creating session', sessionId, error); + return null; + } + }, + + get: async (sessionId) => { + try { + const [rows] = await pool.execute('SELECT * FROM sessions WHERE session_id = ?', [sessionId]); + if (rows.length === 0) return null; + + const row = rows[0]; + return { + ...row, + data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data + }; + } catch (error) { + logToFile('MariaDB: Error getting session', sessionId, error); + return null; + } + }, + + update: async (sessionId, data) => { + try { + const [result] = await pool.execute( + 'UPDATE sessions SET data = ?, updated_at = CURRENT_TIMESTAMP WHERE session_id = ?', + [JSON.stringify(data), sessionId] + ); + return result.affectedRows > 0; + } catch (error) { + logToFile('MariaDB: Error updating session', sessionId, error); + return false; + } + }, + + delete: async (sessionId) => { + try { + const [result] = await pool.execute('DELETE FROM sessions WHERE session_id = ?', [sessionId]); + return result.affectedRows > 0; + } catch (error) { + logToFile('MariaDB: Error deleting session', sessionId, error); + return false; + } + }, + + cleanup: async () => { + try { + const [result] = await pool.execute('DELETE FROM sessions WHERE expires_at < NOW()'); + logToFile('MariaDB: Cleaned up expired sessions', result.affectedRows); + return result.affectedRows; + } catch (error) { + logToFile('MariaDB: Error cleaning up sessions', error); + return 0; + } + } +}; + +// Rules helpers +const rulesHelpers = { + getAll: async () => { + try { + const [rows] = await pool.execute('SELECT * FROM rules ORDER BY id'); + return rows; + } catch (error) { + logToFile('MariaDB: Error getting all rules', error); + return []; + } + }, + + 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] + ); + return result.insertId; + } catch (error) { + logToFile('MariaDB: Error creating rule', error); + return null; + } + }, + + delete: async (id) => { + try { + const [result] = await pool.execute('DELETE FROM rules WHERE id = ?', [id]); + return result.affectedRows > 0; + } catch (error) { + logToFile('MariaDB: Error deleting rule', id, error); + return false; + } + } +}; + +// Rules staging helpers +const stagingHelpers = { + getAll: async () => { + try { + const [rows] = await pool.execute('SELECT * FROM rules_staging ORDER BY id'); + return rows; + } catch (error) { + logToFile('MariaDB: Error getting all staging rules', error); + return []; + } + }, + + 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] + ); + return result.insertId; + } catch (error) { + logToFile('MariaDB: Error creating staging rule', error); + return null; + } + }, + + delete: async (id) => { + try { + const [result] = await pool.execute('DELETE FROM rules_staging WHERE id = ?', [id]); + return result.affectedRows > 0; + } catch (error) { + logToFile('MariaDB: Error deleting staging rule', id, error); + return false; + } + }, + + moveToRules: async (id) => { + try { + const connection = await pool.getConnection(); + await connection.beginTransaction(); + + // Get the staging rule + const [stagingRows] = await connection.execute('SELECT * FROM rules_staging WHERE id = ?', [id]); + if (stagingRows.length === 0) { + await connection.rollback(); + connection.release(); + return false; + } + + const rule = stagingRows[0]; + + // 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] + ); + + // Delete from staging + await connection.execute('DELETE FROM rules_staging WHERE id = ?', [id]); + + await connection.commit(); + connection.release(); + return true; + } catch (error) { + logToFile('MariaDB: Error moving staging rule to rules', id, error); + return false; + } + } +}; + +// Weapons helpers +const weaponsHelpers = { + getAll: async () => { + try { + const [rows] = await pool.execute('SELECT * FROM weapons ORDER BY id'); + return rows; + } catch (error) { + logToFile('MariaDB: Error getting all weapons', error); + return []; + } + } +}; + +// Bestiary helpers +const bestiaryHelpers = { + getAll: async () => { + try { + const [rows] = await pool.execute('SELECT * FROM bestiary ORDER BY id'); + return rows; + } catch (error) { + logToFile('MariaDB: Error getting all bestiary', error); + return []; + } + } +}; + +// Initialize database +createTables().catch(error => { + console.error('Failed to initialize MariaDB:', error); + process.exit(1); +}); + +// Export the connection pool and helpers +module.exports = { + pool, + playerHelpers, + sessionHelpers, + rulesHelpers, + stagingHelpers, + weaponsHelpers, + bestiaryHelpers, + logToFile +}; diff --git a/database/migrate-inventory-to-gear.js b/database/migrate-inventory-to-gear.js new file mode 100644 index 0000000..cb2ead1 --- /dev/null +++ b/database/migrate-inventory-to-gear.js @@ -0,0 +1,56 @@ +const { playerHelpers } = require('./mariadb'); + +async function migrateInventoryToGear() { + try { + console.log('Starting inventory to gear migration...'); + + const players = await playerHelpers.getAll(); + + for (const player of players) { + if (player.tabInfo && player.tabInfo.inventory && player.tabInfo.inventory.length > 0) { + console.log(`Migrating inventory for ${player.name}...`); + + const updatedTabInfo = { ...player.tabInfo }; + + // Initialize gear if it doesn't exist + if (!updatedTabInfo.gear) { + updatedTabInfo.gear = []; + } + + // Move inventory items to gear + for (const invItem of updatedTabInfo.inventory) { + const gearItem = { + name: invItem.name, + qty: invItem.count || invItem.quantity || 1 + }; + + // Check if item already exists in gear + const existingGearIndex = updatedTabInfo.gear.findIndex(g => g.name === gearItem.name); + + if (existingGearIndex >= 0) { + // Update existing item quantity + updatedTabInfo.gear[existingGearIndex].qty += gearItem.qty; + } else { + // Add new item to gear + updatedTabInfo.gear.push(gearItem); + } + } + + // Clear inventory since we moved everything to gear + updatedTabInfo.inventory = []; + + // Update player + await playerHelpers.update(player.name, { ...player, tabInfo: updatedTabInfo }); + console.log(` Moved ${player.tabInfo.inventory.length} items to gear`); + } + } + + console.log('Migration completed!'); + } catch (error) { + console.error('Migration failed:', error); + } + + process.exit(0); +} + +migrateInventoryToGear(); diff --git a/database/migrate-sqlite-to-mariadb.js b/database/migrate-sqlite-to-mariadb.js new file mode 100644 index 0000000..09c1bce --- /dev/null +++ b/database/migrate-sqlite-to-mariadb.js @@ -0,0 +1,392 @@ +#!/usr/bin/env node + +const Database = require('better-sqlite3'); +const mysql = require('mysql2/promise'); +const path = require('path'); + +// SQLite database path +const sqliteDbPath = path.join(__dirname, 'sqlite', 'deathwatch.db'); + +// MariaDB configuration +const mariadbConfig = { + host: 'localhost', + user: 'deathwatch', + password: 'dwroller2025', + database: 'deathwatch' +}; + +// Helper function to convert SQLite datetime to MySQL format +function convertDateTime(dateString) { + if (!dateString) return null; + try { + return new Date(dateString).toISOString().slice(0, 19).replace('T', ' '); + } catch (error) { + console.warn(`Warning: Could not convert datetime: ${dateString}`); + return null; + } +} + +async function migrateData() { + let sqliteDb = null; + let mariadbConnection = null; + + try { + console.log('Starting data migration from SQLite to MariaDB...'); + + // Connect to SQLite + console.log('Connecting to SQLite database...'); + sqliteDb = new Database(sqliteDbPath); + + // Connect to MariaDB + console.log('Connecting to MariaDB database...'); + mariadbConnection = await mysql.createConnection(mariadbConfig); + + // Migrate players table (includes requisition_points and renown_level) + console.log('Migrating players table...'); + const players = sqliteDb.prepare('SELECT * FROM players').all(); + console.log(`Found ${players.length} players to migrate`); + + for (const player of players) { + console.log(` Migrating player: ${player.name}`); + + // Convert text fields to proper JSON + const rollerInfo = player.roller_info || '{}'; + const shopInfo = player.shop_info || '{}'; + const tabInfo = player.tab_info || '{}'; + + await mariadbConnection.execute( + `INSERT INTO players (id, name, roller_info, shop_info, tab_info, pw, pw_hash, created_at, updated_at, requisition_points, renown_level) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + roller_info = VALUES(roller_info), + shop_info = VALUES(shop_info), + tab_info = VALUES(tab_info), + pw = VALUES(pw), + pw_hash = VALUES(pw_hash), + updated_at = VALUES(updated_at), + requisition_points = VALUES(requisition_points), + renown_level = VALUES(renown_level)`, + [ + player.id, + player.name, + rollerInfo, + shopInfo, + tabInfo, + player.pw || '', + player.pw_hash || '', + convertDateTime(player.created_at), + convertDateTime(player.updated_at), + player.requisition_points || 0, + player.renown_level || 'None' + ] + ); + } + + // Migrate sessions table + console.log('Migrating sessions table...'); + const sessions = sqliteDb.prepare('SELECT * FROM sessions').all(); + console.log(`Found ${sessions.length} sessions to migrate`); + + for (const session of sessions) { + console.log(` Migrating session: ${session.session_id}`); + + await mariadbConnection.execute( + `INSERT INTO sessions (id, session_id, data, expires_at, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + data = VALUES(data), + expires_at = VALUES(expires_at), + updated_at = VALUES(updated_at)`, + [ + session.id, + session.session_id, + session.data || '{}', + convertDateTime(session.expires_at), + convertDateTime(session.created_at), + convertDateTime(session.updated_at) + ] + ); + } + + // Migrate shop_items table + console.log('Migrating shop_items table...'); + const shopItems = sqliteDb.prepare('SELECT * FROM shop_items').all(); + console.log(`Found ${shopItems.length} shop items to migrate`); + + for (const item of shopItems) { + console.log(` Migrating shop item: ${item.name}`); + + await mariadbConnection.execute( + `INSERT INTO shop_items (id, name, category, requisition_cost, renown_requirement, item_type, stats, source, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + name = VALUES(name), + category = VALUES(category), + requisition_cost = VALUES(requisition_cost), + renown_requirement = VALUES(renown_requirement), + item_type = VALUES(item_type), + stats = VALUES(stats), + source = VALUES(source), + updated_at = VALUES(updated_at)`, + [ + item.id, + item.name, + item.category, + item.requisition_cost, + item.renown_requirement, + item.item_type, + item.stats, + item.source, + convertDateTime(item.created_at), + convertDateTime(item.updated_at) + ] + ); + } + + // Migrate player_inventory table + console.log('Migrating player_inventory table...'); + const playerInventory = sqliteDb.prepare('SELECT * FROM player_inventory').all(); + console.log(`Found ${playerInventory.length} inventory items to migrate`); + + for (const invItem of playerInventory) { + console.log(` Migrating inventory item for player ${invItem.player_id}`); + + await mariadbConnection.execute( + `INSERT INTO player_inventory (id, player_id, item_id, quantity, acquired_at, transaction_details) + VALUES (?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + quantity = VALUES(quantity), + transaction_details = VALUES(transaction_details)`, + [ + invItem.id, + invItem.player_id, + invItem.item_id, + invItem.quantity, + convertDateTime(invItem.acquired_at), + invItem.transaction_details + ] + ); + } + + // Migrate transactions table + console.log('Migrating transactions table...'); + const transactions = sqliteDb.prepare('SELECT * FROM transactions').all(); + console.log(`Found ${transactions.length} transactions to migrate`); + + for (const transaction of transactions) { + console.log(` Migrating transaction ${transaction.id}`); + + await mariadbConnection.execute( + `INSERT INTO transactions (id, player_id, item_id, requisition_cost, previous_rp, new_rp, transaction_date) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [ + transaction.id, + transaction.player_id, + transaction.item_id, + transaction.requisition_cost, + transaction.previous_rp, + transaction.new_rp, + convertDateTime(transaction.transaction_date) + ] + ); + } + + // Migrate armour table + console.log('Migrating armour table...'); + const armour = sqliteDb.prepare('SELECT * FROM armour').all(); + console.log(`Found ${armour.length} armour items to migrate`); + + for (const armourItem of armour) { + console.log(` Migrating armour: ${armourItem.name}`); + + await mariadbConnection.execute( + `INSERT INTO armour (id, name, req, renown, category, stats, source, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + req = VALUES(req), + renown = VALUES(renown), + category = VALUES(category), + stats = VALUES(stats), + source = VALUES(source)`, + [ + armourItem.id, + armourItem.name, + armourItem.req, + armourItem.renown, + armourItem.category, + armourItem.stats, + armourItem.source, + convertDateTime(armourItem.created_at) + ] + ); + } + + // Migrate weapons table + console.log('Migrating weapons table...'); + const weapons = sqliteDb.prepare('SELECT * FROM weapons').all(); + console.log(`Found ${weapons.length} weapons to migrate`); + + for (const weapon of weapons) { + console.log(` Migrating weapon: ${weapon.name}`); + + await mariadbConnection.execute( + `INSERT INTO weapons (id, name, req, renown, category, stats, source, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + req = VALUES(req), + renown = VALUES(renown), + category = VALUES(category), + stats = VALUES(stats), + source = VALUES(source)`, + [ + weapon.id, + weapon.name, + weapon.req, + weapon.renown, + weapon.category, + weapon.stats, + weapon.source, + convertDateTime(weapon.created_at) + ] + ); + } + + // Migrate bestiary table + console.log('Migrating bestiary table...'); + const bestiary = sqliteDb.prepare('SELECT * FROM bestiary').all(); + console.log(`Found ${bestiary.length} bestiary entries to migrate`); + + for (const beast of bestiary) { + console.log(` Migrating bestiary: ${beast.name}`); + + await mariadbConnection.execute( + `INSERT INTO bestiary (id, name, book, page, pdf, stats, profile, snippet, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + book = VALUES(book), + page = VALUES(page), + pdf = VALUES(pdf), + stats = VALUES(stats), + profile = VALUES(profile), + snippet = VALUES(snippet)`, + [ + beast.id, + beast.name, + beast.book, + beast.page, + beast.pdf, + beast.stats, + beast.profile, + beast.snippet, + convertDateTime(beast.created_at) + ] + ); + } + + // Migrate rules table + console.log('Migrating rules table...'); + const rules = sqliteDb.prepare('SELECT * FROM rules').all(); + console.log(`Found ${rules.length} rules to migrate`); + + for (const rule of rules) { + console.log(` Migrating rule: ${rule.title}`); + + // Truncate rule_id if it's too long (max 255 chars) + const ruleId = rule.rule_id ? rule.rule_id.substring(0, 255) : null; + // Truncate title if it's too long (max 500 chars) + const title = rule.title ? rule.title.substring(0, 500) : null; + + await mariadbConnection.execute( + `INSERT INTO rules (id, rule_id, title, content, page, source, source_abbr, category, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + title = VALUES(title), + content = VALUES(content), + page = VALUES(page), + source = VALUES(source), + source_abbr = VALUES(source_abbr), + category = VALUES(category)`, + [ + rule.id, + ruleId, + title, + rule.content, + rule.page, + rule.source, + rule.source_abbr, + rule.category, + convertDateTime(rule.created_at) + ] + ); + } + + // Migrate rules_staging table + console.log('Migrating rules_staging table...'); + const rulesStaging = sqliteDb.prepare('SELECT * FROM rules_staging').all(); + console.log(`Found ${rulesStaging.length} rules_staging entries to migrate`); + + for (const rule of rulesStaging) { + console.log(` Migrating rules_staging: ${rule.title}`); + + await mariadbConnection.execute( + `INSERT INTO rules_staging (id, title, content, category, page, original_json, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + title = VALUES(title), + content = VALUES(content), + category = VALUES(category), + page = VALUES(page), + original_json = VALUES(original_json)`, + [ + rule.id, + rule.title, + rule.content, + rule.category, + rule.page, + rule.original_json, + convertDateTime(rule.created_at) + ] + ); + } + + console.log('Migration completed successfully!'); + + // Verify migration + console.log('\nVerifying migration...'); + const [mariadbPlayers] = await mariadbConnection.execute('SELECT COUNT(*) as count FROM players'); + const [mariadbSessions] = await mariadbConnection.execute('SELECT COUNT(*) as count FROM sessions'); + const [mariadbShopItems] = await mariadbConnection.execute('SELECT COUNT(*) as count FROM shop_items'); + const [mariadbInventory] = await mariadbConnection.execute('SELECT COUNT(*) as count FROM player_inventory'); + const [mariadbTransactions] = await mariadbConnection.execute('SELECT COUNT(*) as count FROM transactions'); + const [mariadbArmour] = await mariadbConnection.execute('SELECT COUNT(*) as count FROM armour'); + const [mariadbWeapons] = await mariadbConnection.execute('SELECT COUNT(*) as count FROM weapons'); + const [mariadbBestiary] = await mariadbConnection.execute('SELECT COUNT(*) as count FROM bestiary'); + const [mariadbRules] = await mariadbConnection.execute('SELECT COUNT(*) as count FROM rules'); + const [mariadbRulesStaging] = await mariadbConnection.execute('SELECT COUNT(*) as count FROM rules_staging'); + + console.log(`MariaDB players: ${mariadbPlayers[0].count}`); + console.log(`MariaDB sessions: ${mariadbSessions[0].count}`); + console.log(`MariaDB shop_items: ${mariadbShopItems[0].count}`); + console.log(`MariaDB player_inventory: ${mariadbInventory[0].count}`); + console.log(`MariaDB transactions: ${mariadbTransactions[0].count}`); + console.log(`MariaDB armour: ${mariadbArmour[0].count}`); + console.log(`MariaDB weapons: ${mariadbWeapons[0].count}`); + console.log(`MariaDB bestiary: ${mariadbBestiary[0].count}`); + console.log(`MariaDB rules: ${mariadbRules[0].count}`); + console.log(`MariaDB rules_staging: ${mariadbRulesStaging[0].count}`); + + } catch (error) { + console.error('Migration failed:', error); + process.exit(1); + } finally { + if (sqliteDb) { + sqliteDb.close(); + } + if (mariadbConnection) { + await mariadbConnection.end(); + } + } +} + +// Run migration +migrateData().catch(console.error); diff --git a/database/migrate-to-sqlite.js b/database/migrate-to-sqlite.js deleted file mode 100644 index e69de29..0000000 diff --git a/database/routes/bestiaryRoutes.js b/database/routes/bestiaryRoutes.js index fc66dc5..f53839c 100644 --- a/database/routes/bestiaryRoutes.js +++ b/database/routes/bestiaryRoutes.js @@ -1,18 +1,26 @@ const express = require('express'); const fs = require('fs'); const path = require('path'); -const { db } = require('../sqlite-db'); +const { bestiaryHelpers, logToFile } = require('../mariadb'); const router = express.Router(); -// Read bestiary from sqlite table `bestiary` -function loadBestiaryData() { +// Read bestiary from MariaDB table `bestiary` +async function loadBestiaryData() { try { - const rows = db.prepare('SELECT id,name,book,page,pdf,stats,profile,snippet FROM bestiary ORDER BY name').all(); + const rows = await bestiaryHelpers.getAll(); return rows.map(r => { let stats = {}; - try { stats = JSON.parse(r.stats || '{}'); } catch(e){} + try { + stats = typeof r.stats === 'string' ? JSON.parse(r.stats) : r.stats || {}; + } catch(e){ + logToFile('Error parsing bestiary stats for', r.name, e); + } let profile = {}; - try { profile = JSON.parse(r.profile || '{}'); } catch(e){} + try { + profile = typeof r.profile === 'string' ? JSON.parse(r.profile) : r.profile || {}; + } catch(e){ + logToFile('Error parsing bestiary profile for', r.name, e); + } return { _id: r.id, bestiaryName: r.name, @@ -25,7 +33,8 @@ function loadBestiaryData() { }; }); } catch (e) { - console.error('Failed to load bestiary from sqlite:', e); + console.error('Failed to load bestiary from MariaDB:', e); + logToFile('Error loading bestiary:', e); return []; } } @@ -88,9 +97,9 @@ function transformBestiaryEntry(entry) { } // Get all bestiary entries formatted for dice roller -router.get('/enemies', (req, res) => { +router.get('/enemies', async (req, res) => { try { - const entries = loadBestiaryData(); + const entries = await loadBestiaryData(); // Transform entries for dice roller format const enemies = entries @@ -106,43 +115,46 @@ router.get('/enemies', (req, res) => { res.json(enemies); } catch (error) { console.error('Bestiary enemies error:', error); + logToFile('Error getting bestiary enemies:', error); res.status(500).json({ error: 'Failed to get enemies' }); } }); // Get full bestiary data (for bestiary tab) -router.get('/full', (req, res) => { +router.get('/full', async (req, res) => { try { - const entries = loadBestiaryData(); + const entries = await loadBestiaryData(); res.json(entries); } catch (error) { console.error('Bestiary full error:', error); + logToFile('Error getting full bestiary:', error); res.status(500).json({ error: 'Failed to get bestiary data' }); } }); // Get bestiary statistics -router.get('/stats', (req, res) => { +router.get('/stats', async (req, res) => { try { - const entries = loadBestiaryData(); + const entries = await loadBestiaryData(); const stats = { totalEntries: entries.length, withValidStats: entries.filter(e => e.stats?.profile?.t).length, withWounds: entries.filter(e => e.wounds || e.stats?.wounds).length, books: [...new Set(entries.map(e => e.book).filter(Boolean))], - lastUpdated: new Date(lastLoaded).toISOString() + lastUpdated: new Date().toISOString() }; res.json(stats); } catch (error) { console.error('Bestiary stats error:', error); + logToFile('Error getting bestiary stats:', error); res.status(500).json({ error: 'Failed to get bestiary stats' }); } }); // Force reload bestiary data (admin only) -router.post('/reload', (req, res) => { +router.post('/reload', async (req, res) => { try { // Check for GM secret const gmSecret = req.headers['x-gm-secret']; @@ -150,19 +162,17 @@ router.post('/reload', (req, res) => { return res.status(403).json({ error: 'Unauthorized' }); } - // Reset cache - bestiaryData = null; - lastLoaded = 0; - - const entries = loadBestiaryData(); + // No cache to reset since we query MariaDB directly + const entries = await loadBestiaryData(); res.json({ success: true, totalEntries: entries.length, - message: 'Bestiary data reloaded successfully' + message: 'Bestiary data reloaded from MariaDB successfully' }); } catch (error) { console.error('Bestiary reload error:', error); + logToFile('Error reloading bestiary:', error); res.status(500).json({ error: 'Failed to reload bestiary data' }); } }); diff --git a/database/routes/playerRoutes-sqlite.js b/database/routes/playerRoutes-sqlite.js deleted file mode 100644 index 1e09d10..0000000 --- a/database/routes/playerRoutes-sqlite.js +++ /dev/null @@ -1,676 +0,0 @@ -const express = require('express'); -const { playerHelpers, sessionHelpers, db } = require('../sqlite-db'); -const fs = require('fs'); -const path = require('path'); - -// shop data will be read from sqlite `shop_items` table when needed - -// Simple file logger -function logToFile(...args) { - const msg = `[${new Date().toISOString()}] ` + args.map(a => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ') + '\n'; - fs.appendFileSync(path.join(__dirname, '../backend.log'), msg, { encoding: 'utf8' }); -} - -const requireSession = require('../requireSession'); -const { validatePlayer } = require('../validate'); -const router = express.Router(); - -// PUBLIC ROUTES - before session middleware -// Get shop inventory -router.get('/shop', (req, res) => { - try { - console.log('Shop endpoint hit'); - // Query sqlite shop_items and return grouped by category - const items = db.prepare('SELECT id, name, category, requisition_cost as req, renown_requirement as renown, item_type, stats, source FROM shop_items ORDER BY category, name').all(); - const grouped = items.reduce((acc, it) => { - acc[it.category] = acc[it.category] || []; - let stats = {}; - try { stats = JSON.parse(it.stats || '{}'); } catch(e){} - acc[it.category].push({ id: it.id, name: it.name, req: it.req, renown: it.renown, itemType: it.item_type, stats, source: it.source }); - return acc; - }, {}); - res.json(grouped); - } catch (error) { - console.error('Shop error:', error); - logToFile('API: Failed to get shop data', error); - res.status(500).json({ error: String(error) }); - } -}); - -// Get player names for login dropdown (public - no session required) -router.get('/names', (req, res) => { - try { - console.log('Player names endpoint hit'); - const players = playerHelpers.getAll(); - // Only return names for the login dropdown, not full player data - const playerNames = players.map(p => ({ name: p.name })); - res.json(playerNames); - } catch (error) { - console.error('Player names error:', error); - logToFile('API: Failed to get player names', error); - res.status(500).json({ error: String(error) }); - } -}); - -// Login endpoint - create a server session so x-session-id can be used (public - no session required) -router.post('/login', async (req, res) => { - try { - const { name, password } = req.body; - - if (!name || !password) { - return res.status(400).json({ error: 'Name and password are required' }); - } - - const player = playerHelpers.getByName(name); - if (!player) { - return res.status(401).json({ error: 'Invalid credentials' }); - } - - // Check password using safeCompare - const isValidPassword = await safeCompare(password, player.pwHash); - if (!isValidPassword) { - return res.status(401).json({ error: 'Invalid credentials' }); - } - - // Create session and store it in sessions table - const sessionId = require('crypto').randomBytes(32).toString('hex'); - const expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(); // 24h - sessionHelpers.create(sessionId, { playerName: player.name }, expiresAt); - - logToFile('API: Player login', name, sessionId); - res.json({ - message: 'Login successful', - sessionId, - expiresAt, - player: { - name: player.name, - rollerInfo: player.rollerInfo, - shopInfo: player.shopInfo, - tabInfo: player.tabInfo - } - }); - } catch (error) { - logToFile('API: Login failed', req.body.name, error && error.stack ? error.stack : error); - res.status(500).json({ error: 'Login failed' }); - } -}); - -// Apply session middleware to all routes EXCEPT those above this line -router.use(requireSession); - -// Add safe bcrypt helpers to avoid MODULE_NOT_FOUND failures at runtime -async function safeHash(pw) { - if (!pw) return ''; - try { - const bcrypt = require('bcrypt'); - return await bcrypt.hash(pw, 10); - } catch (err) { - // Fallback to storing plaintext (development only) and log the error - logToFile('WARN: bcrypt.hash failed, falling back to plaintext pw', err && err.stack ? err.stack : String(err)); - return String(pw); - } -} - -async function safeCompare(candidate, hashed) { - try { - const bcrypt = require('bcrypt'); - return await bcrypt.compare(candidate, hashed); - } catch (err) { - // If bcrypt not available, fall back to plaintext comparison (dev only) - logToFile('WARN: bcrypt.compare failed, falling back to plaintext compare', err && err.stack ? err.stack : String(err)); - return String(candidate) === String(hashed); - } -} - -// TEMP ADMIN: List all users -router.get('/admin/list', async (req, res) => { - try { - const players = playerHelpers.getAll(); - res.json(players); - } catch (error) { - logToFile('API: Failed to list players', error); - res.status(500).json({ error: 'Failed to list players' }); - } -}); - -// Expose pre-generated Space Marine names for GM use -router.get('/admin/pregens', async (req, res) => { - try { - const filePath = path.join(__dirname, '..', 'pregen_names.json'); - if (!fs.existsSync(filePath)) return res.json([]); - const names = JSON.parse(fs.readFileSync(filePath, 'utf8')); - res.json(names); - } catch (error) { - logToFile('API: Failed to get pregens', error); - res.status(500).json({ error: 'Failed to get pregens' }); - } -}); - -// Shop endpoint already defined above - -// TEMP ADMIN: Delete test users (name contains 'test' or 'Test') -router.delete('/admin/delete-tests', async (req, res) => { - try { - const players = playerHelpers.getAll(); - const testPlayers = players.filter(p => /test/i.test(p.name)); - - let deletedCount = 0; - for (const player of testPlayers) { - if (playerHelpers.delete(player.name)) { - deletedCount++; - } - } - - res.json({ deletedCount }); - } catch (error) { - logToFile('API: Failed to delete test users', error); - res.status(500).json({ error: 'Failed to delete test users' }); - } -}); - -// Get all players (public for dropdown) -router.get('/', async (req, res) => { - try { - const players = playerHelpers.getAll(); - logToFile('API: Fetch all players (public)', `Found ${players.length} players`); - - // Allow full list if client provided a valid session id OR the GM secret header - const gmSecret = req.headers['x-gm-secret'] || req.query.gmSecret || (req.body && req.body.gmSecret); - const gmPassword = process.env.GM_PASSWORD || 'bongo'; - const isGm = gmSecret && String(gmSecret) === String(gmPassword); - const isAuthed = !!req.headers['x-session-id']; - - if (!isAuthed && !isGm) { - return res.json(players.map(p => ({ name: p.name }))); - } - - res.json(players); - } catch (error) { - logToFile('API: Failed to fetch players', error); - res.status(500).json({ error: 'Failed to fetch players' }); - } -}); - -// Get single player by name -router.get('/:name', async (req, res) => { - try { - const player = playerHelpers.getByName(req.params.name); - if (!player) { - return res.status(404).json({ error: 'Player not found' }); - } - logToFile('API: Fetch player', req.params.name); - res.json(player); - } catch (error) { - logToFile('API: Failed to fetch player', req.params.name, error); - res.status(500).json({ error: 'Failed to fetch player' }); - } -}); - -// Create new player -router.post('/', async (req, res) => { - try { - const { name, pw, ...otherData } = req.body; - - if (!name) { - return res.status(400).json({ error: 'Player name is required' }); - } - - // Check if player already exists - const existingPlayer = playerHelpers.getByName(name); - if (existingPlayer) { - return res.status(409).json({ error: 'Player already exists' }); - } - - // If a plain password was provided, hash it first so validation won't reject plaintext - let pwHash = ''; - if (pw) { - pwHash = await safeHash(pw); - } - - // Validate and normalize incoming player object. Pass pwHash instead of plaintext pw. - const { valid, errors, normalized } = validatePlayer({ name, pwHash, ...otherData }); - if (!valid) { - return res.status(400).json({ error: 'Validation failed', details: errors }); - } - - const newPlayer = playerHelpers.create({ - name: normalized.name, - // Do NOT store plaintext pw to avoid validation rejecting records later - pw: '', - pwHash, - rollerInfo: normalized.rollerInfo || {}, - shopInfo: normalized.shopInfo || {}, - tabInfo: normalized.tabInfo || {} - }); - - logToFile('API: Created player', name); - res.status(201).json(newPlayer); - } catch (error) { - // Log stack for easier debugging - logToFile('API: Failed to create player', req.body && req.body.name, error && error.stack ? error.stack : error); - res.status(500).json({ error: 'Failed to create player' }); - } -}); - -// Update player -router.put('/:name', requireSession, async (req, res) => { - try { - const { name } = req.params; - // Ensure updateData is an object to avoid runtime TypeErrors when fields are missing - const updateData = req.body || {}; - - // Check if player exists - const existingPlayer = playerHelpers.getByName(name); - if (!existingPlayer) { - return res.status(404).json({ error: 'Player not found' }); - } - - // Handle password update if provided using safeHash - if (updateData.pw) { - updateData.pwHash = await safeHash(updateData.pw); - // Do not pass plaintext pw into validation; validation requires pwHash only - // We'll store an empty pw field (frontend may still use pw for temporary purposes) - updateData.pw = ''; - } - - // Merge the data properly - const mergedData = { - rollerInfo: { ...existingPlayer.rollerInfo, ...(updateData.rollerInfo || {}) }, - shopInfo: { ...existingPlayer.shopInfo, ...(updateData.shopInfo || {}) }, - tabInfo: { ...existingPlayer.tabInfo, ...(updateData.tabInfo || {}) }, - // Never carry forward plaintext pw into validation; keep pw empty and use pwHash - pw: '', - pwHash: updateData.pwHash || existingPlayer.pwHash - }; - - // If frontend sends flat fields (not under tabInfo), map them into tabInfo. - // This accepts updates for any of the known tabInfo keys even when only those - // fields are sent, not requiring playerName/charName to be present. - const flatFields = [ - 'playerName','charName','gear','chapter','demeanour','speciality','rank','powerArmour', - 'description','pastEvent','personalDemeanour','characteristics','skills','weapons','armour', - 'talents','psychic','wounds','insanity','movement','fate','corruption','renown','xp','xpSpent', - 'notes','rp' - ]; - - for (const key of flatFields) { - if (Object.prototype.hasOwnProperty.call(updateData, key)) { - mergedData.tabInfo[key] = updateData[key]; - } - } - - // Validate merged data before applying - const { valid: v2, errors: e2, normalized: normalized2 } = validatePlayer(Object.assign({ name }, mergedData)); - if (!v2) { - return res.status(400).json({ error: 'Validation failed', details: e2 }); - } - - const updated = playerHelpers.update(name, normalized2); - - if (!updated) { - return res.status(500).json({ error: 'Failed to update player' }); - } - - const updatedPlayer = playerHelpers.getByName(name); - logToFile('API: Updated player', name); - res.json(updatedPlayer); - } catch (error) { - logToFile('API: Failed to update player', req.params.name, error && error.stack ? error.stack : error); - res.status(500).json({ error: 'Failed to update player' }); - } -}); - -// Delete player -router.delete('/:name', requireSession, async (req, res) => { - try { - const { name } = req.params; - - const deleted = playerHelpers.delete(name); - - if (!deleted) { - return res.status(404).json({ error: 'Player not found' }); - } - - logToFile('API: Deleted player', name); - res.json({ message: 'Player deleted successfully' }); - } catch (error) { - logToFile('API: Failed to delete player', req.params.name, error); - res.status(500).json({ error: 'Failed to delete player' }); - } -}); - -// Upload avatar (base64 JSON payload) - saves to public/avatars and updates tabInfo.picture -router.post('/:name/avatar', requireSession, async (req, res) => { - try { - const { name } = req.params; - const { filename, data } = req.body || {}; - - if (!filename || !data) return res.status(400).json({ error: 'filename and data required' }); - - const existingPlayer = playerHelpers.getByName(name); - if (!existingPlayer) return res.status(404).json({ error: 'Player not found' }); - - // Extract base64 payload if data URL provided - const match = String(data).match(/^data:(image\/(png|jpeg|jpg|gif));base64,(.*)$/i); - let mimeType = null; - let base64 = null; - if (match) { - mimeType = match[1]; - base64 = match[3]; - } else { - // Assume raw base64 and try to infer extension from filename - base64 = String(data).replace(/^\s+|\s+$/g, ''); - } - - // Validate size (limit to 200KB) - let buffer; - try { - buffer = Buffer.from(base64, 'base64'); - } catch (err) { - return res.status(400).json({ error: 'Invalid base64 data' }); - } - const MAX_BYTES = 200 * 1024; - if (buffer.length > MAX_BYTES) return res.status(413).json({ error: 'File too large' }); - - // Sanitize filename and ensure extension - const ext = path.extname(filename).toLowerCase() || (mimeType ? `.${mimeType.split('/')[1]}` : '.png'); - const safeName = `${existingPlayer.name.replace(/[^a-z0-9_-]/gi, '_')}_${Date.now()}${ext}`; - const avatarsDir = path.join(__dirname, '..', '..', 'public', 'avatars'); - if (!fs.existsSync(avatarsDir)) fs.mkdirSync(avatarsDir, { recursive: true }); - - const outPath = path.join(avatarsDir, safeName); - fs.writeFileSync(outPath, buffer); - - // Update player's tabInfo.picture to public URL path - const avatarUrl = `/avatars/${safeName}`; - - const mergedData = { - rollerInfo: { ...existingPlayer.rollerInfo }, - shopInfo: { ...existingPlayer.shopInfo }, - tabInfo: { ...(existingPlayer.tabInfo || {}), picture: avatarUrl }, - // Do not include plaintext pw when validating/updating avatar - pw: '', - pwHash: existingPlayer.pwHash - }; - - // Validate then update - const { valid, errors, normalized } = validatePlayer(Object.assign({ name }, mergedData)); - if (!valid) return res.status(400).json({ error: 'Validation failed', details: errors }); - - const ok = playerHelpers.update(name, normalized); - if (!ok) return res.status(500).json({ error: 'Failed to update player with avatar' }); - - const updated = playerHelpers.getByName(name); - logToFile('API: Uploaded avatar for', name, avatarUrl); - res.json(updated); - } catch (error) { - logToFile('API: Avatar upload failed', req.params.name, error && error.stack ? error.stack : error); - res.status(500).json({ error: 'Avatar upload failed' }); - } -}); - -// GM ENDPOINTS - bypassing session validation with GM secret -function gmBypass(req, res, next) { - const gmSecret = req.headers['x-gm-secret']; - if (gmSecret === 'bongo') { - logToFile('SESSION: GM bypass accepted', req.method, req.url); - return next(); - } - logToFile('SESSION: GM bypass rejected - invalid secret', req.method, req.url); - return res.status(401).json({ error: 'GM access denied' }); -} - -// Add/update player (GM only) -router.post('/gm/add-or-update', gmBypass, (req, res) => { - try { - const { name, rp, pw } = req.body; - - if (!name) { - return res.status(400).json({ error: 'Player name is required' }); - } - - // Check if player exists - const existing = playerHelpers.getByName(name); - - if (existing) { - // Update existing player - const updates = { ...existing }; - if (rp !== undefined) updates.tabInfo = { ...updates.tabInfo, rp: parseInt(rp) }; - if (pw) updates.pwHash = require('bcrypt').hashSync(pw, 10); - - const { valid, errors, normalized } = validatePlayer(updates); - if (!valid) return res.status(400).json({ error: 'Validation failed', details: errors }); - - const ok = playerHelpers.update(name, normalized); - if (!ok) return res.status(500).json({ error: 'Failed to update player' }); - - logToFile('GM: Updated player', name); - return res.json({ success: true, message: `Updated player ${name}` }); - } else { - // Create new player - const defaultRP = rp !== undefined ? parseInt(rp) : 50; - const password = pw || '1234'; - - const newPlayer = { - name, - tabInfo: { - rp: defaultRP, - renown: 'None', - xp: 0, - xpSpent: 0, - charName: `Brother ${name.charAt(0).toUpperCase() + name.slice(1)}` - }, - rollerInfo: {}, - shopInfo: {}, - pwHash: require('bcrypt').hashSync(password, 10), - pw: '' - }; - - const { valid, errors, normalized } = validatePlayer(newPlayer); - if (!valid) return res.status(400).json({ error: 'Validation failed', details: errors }); - - const saved = playerHelpers.create(normalized); - if (!saved) return res.status(500).json({ error: 'Failed to create player' }); - - logToFile('GM: Created player', name); - return res.json({ success: true, message: `Created player ${name}` }); - } - } catch (error) { - logToFile('GM: Add/update player failed', error); - res.status(500).json({ error: 'Failed to add/update player' }); - } -}); - -// Set RP (GM only) -router.post('/gm/set-rp', gmBypass, (req, res) => { - try { - const { playerName, requisitionPoints } = req.body; - - if (!playerName) { - return res.status(400).json({ error: 'Player name is required' }); - } - - const player = playerHelpers.getByName(playerName); - if (!player) { - return res.status(404).json({ error: 'Player not found' }); - } - - const updates = { - ...player, - tabInfo: { ...player.tabInfo, rp: parseInt(requisitionPoints) } - }; - - const { valid, errors, normalized } = validatePlayer(updates); - if (!valid) return res.status(400).json({ error: 'Validation failed', details: errors }); - - const ok = playerHelpers.update(playerName, normalized); - if (!ok) return res.status(500).json({ error: 'Failed to update player RP' }); - - logToFile('GM: Set RP for', playerName, 'to', requisitionPoints); - res.json({ success: true, message: `Set RP for ${playerName} to ${requisitionPoints}` }); - } catch (error) { - logToFile('GM: Set RP failed', error); - res.status(500).json({ error: 'Failed to set RP' }); - } -}); - -// Set XP (GM only) -router.post('/gm/set-xp', gmBypass, (req, res) => { - try { - const { playerName, xp } = req.body; - - if (!playerName) { - return res.status(400).json({ error: 'Player name is required' }); - } - - const player = playerHelpers.getByName(playerName); - if (!player) { - return res.status(404).json({ error: 'Player not found' }); - } - - const updates = { - ...player, - tabInfo: { ...player.tabInfo, xp: parseInt(xp) } - }; - - const { valid, errors, normalized } = validatePlayer(updates); - if (!valid) return res.status(400).json({ error: 'Validation failed', details: errors }); - - const ok = playerHelpers.update(playerName, normalized); - if (!ok) return res.status(500).json({ error: 'Failed to update player XP' }); - - logToFile('GM: Set XP for', playerName, 'to', xp); - res.json({ success: true, message: `Set XP for ${playerName} to ${xp}` }); - } catch (error) { - logToFile('GM: Set XP failed', error); - res.status(500).json({ error: 'Failed to set XP' }); - } -}); - -// Set XP Spent (GM only) -router.post('/gm/set-xp-spent', gmBypass, (req, res) => { - try { - const { playerName, xpSpent } = req.body; - - if (!playerName) { - return res.status(400).json({ error: 'Player name is required' }); - } - - const player = playerHelpers.getByName(playerName); - if (!player) { - return res.status(404).json({ error: 'Player not found' }); - } - - const updates = { - ...player, - tabInfo: { ...player.tabInfo, xpSpent: parseInt(xpSpent) } - }; - - const { valid, errors, normalized } = validatePlayer(updates); - if (!valid) return res.status(400).json({ error: 'Validation failed', details: errors }); - - const ok = playerHelpers.update(playerName, normalized); - if (!ok) return res.status(500).json({ error: 'Failed to update player XP Spent' }); - - logToFile('GM: Set XP Spent for', playerName, 'to', xpSpent); - res.json({ success: true, message: `Set XP Spent for ${playerName} to ${xpSpent}` }); - } catch (error) { - logToFile('GM: Set XP Spent failed', error); - res.status(500).json({ error: 'Failed to set XP Spent' }); - } -}); - -// Set Renown (GM only) -router.post('/gm/set-renown', gmBypass, (req, res) => { - try { - const { playerName, renown } = req.body; - - if (!playerName) { - return res.status(400).json({ error: 'Player name is required' }); - } - - const player = playerHelpers.getByName(playerName); - if (!player) { - return res.status(404).json({ error: 'Player not found' }); - } - - const updates = { - ...player, - tabInfo: { ...player.tabInfo, renown } - }; - - const { valid, errors, normalized } = validatePlayer(updates); - if (!valid) return res.status(400).json({ error: 'Validation failed', details: errors }); - - const ok = playerHelpers.update(playerName, normalized); - if (!ok) return res.status(500).json({ error: 'Failed to update player renown' }); - - logToFile('GM: Set renown for', playerName, 'to', renown); - res.json({ success: true, message: `Set renown for ${playerName} to ${renown}` }); - } catch (error) { - logToFile('GM: Set renown failed', error); - res.status(500).json({ error: 'Failed to set renown' }); - } -}); - -// Reset password (GM only) -router.post('/gm/reset-password', gmBypass, (req, res) => { - try { - const { playerName, newPassword } = req.body; - - if (!playerName) { - return res.status(400).json({ error: 'Player name is required' }); - } - - const player = playerHelpers.getByName(playerName); - if (!player) { - return res.status(404).json({ error: 'Player not found' }); - } - - const password = newPassword || '1234'; - const updates = { - ...player, - pwHash: require('bcrypt').hashSync(password, 10), - pw: '' - }; - - const { valid, errors, normalized } = validatePlayer(updates); - if (!valid) return res.status(400).json({ error: 'Validation failed', details: errors }); - - const ok = playerHelpers.update(playerName, normalized); - if (!ok) return res.status(500).json({ error: 'Failed to reset password' }); - - logToFile('GM: Reset password for', playerName); - res.json({ success: true, message: `Reset password for ${playerName}` }); - } catch (error) { - logToFile('GM: Reset password failed', error); - res.status(500).json({ error: 'Failed to reset password' }); - } -}); - -// Delete player (GM only) -router.delete('/gm/delete/:playerName', gmBypass, (req, res) => { - try { - const { playerName } = req.params; - - if (!playerName) { - return res.status(400).json({ error: 'Player name is required' }); - } - - const player = playerHelpers.getByName(playerName); - if (!player) { - return res.status(404).json({ error: 'Player not found' }); - } - - const ok = playerHelpers.delete(playerName); - if (!ok) return res.status(500).json({ error: 'Failed to delete player' }); - - logToFile('GM: Deleted player', playerName); - res.json({ success: true, message: `Deleted player ${playerName}` }); - } catch (error) { - logToFile('GM: Delete player failed', error); - res.status(500).json({ error: 'Failed to delete player' }); - } -}); - -module.exports = router; diff --git a/database/routes/playerRoutes.js b/database/routes/playerRoutes.js old mode 100755 new mode 100644 index c51abae..c9f5bf1 --- a/database/routes/playerRoutes.js +++ b/database/routes/playerRoutes.js @@ -1,151 +1,176 @@ - - const express = require('express'); -const Player = require('../playerModel'); -const fs = require('fs'); -const path = require('path'); - -// Simple file logger -function logToFile(...args) { - const msg = `[${new Date().toISOString()}] ` + args.map(a => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ') + '\n'; - fs.appendFileSync(path.join(__dirname, '../backend.log'), msg, { encoding: 'utf8' }); -} - -const requireSession = require('../requireSession'); +const { playerHelpers, logToFile } = require('../mariadb'); const router = express.Router(); -// TEMP ADMIN: List all users -router.get('/admin/list', async (req, res) => { +// Login endpoint for players +router.post('/login', async (req, res) => { try { - const players = await Player.find(); - res.json(players); + const { name, password } = req.body; + console.log('Login attempt for player:', name); + + if (!name || !password) { + return res.status(400).json({ error: 'Name and password required' }); + } + + // Special handling for GM user + if (name.toLowerCase() === 'gm') { + if (password !== 'bongo') { + return res.status(401).json({ error: 'Invalid password' }); + } + } else { + // For regular players, use password '1234' + if (password !== '1234') { + return res.status(401).json({ error: 'Invalid password' }); + } + } + + const player = await playerHelpers.getByName(name); + if (!player) { + return res.status(404).json({ error: 'Player not found' }); + } + + // Generate a simple session ID (in production, use proper session management) + const sessionId = `session_${name}_${Date.now()}`; + + logToFile('API: Player login', name, 'success'); + res.json({ + success: true, + sessionId, + player: { name: player.name } + }); } catch (error) { - res.status(500).json({ error: 'Failed to list players' }); + console.error('Login error:', error); + logToFile('API: Failed to login player', req.body?.name, error); + res.status(500).json({ error: String(error) }); } }); -// TEMP ADMIN: Delete test users (name contains 'test' or 'Test') -router.delete('/admin/delete-tests', async (req, res) => { +// Get player names for login dropdown (public - no session required) +router.get('/names', async (req, res) => { try { - const result = await Player.deleteMany({ name: /test/i }); - res.json({ deletedCount: result.deletedCount }); + console.log('Player names endpoint hit'); + const players = await playerHelpers.getAll(); + // Only return names for the login dropdown, not full player data + const playerNames = players.map(p => ({ name: p.name })); + res.json(playerNames); } catch (error) { - res.status(500).json({ error: 'Failed to delete test users' }); + console.error('Player names error:', error); + logToFile('API: Failed to get player names', error); + res.status(500).json({ error: String(error) }); } }); // Get all players (public for dropdown) router.get('/', async (req, res) => { try { - const players = await Player.find(); - logToFile('API: Fetch all players (public)', players); - // Only send name for dropdown if not authed - if (!req.headers['x-session-id']) { - return res.json(players.map(p => ({ name: p.name }))); + const players = await playerHelpers.getAll(); + logToFile('API: Fetch all players (public)', players.length); + + // If authenticated (has session header or x-gm-secret), return full player data + const hasSession = req.headers['x-session-id'] || req.headers['x-gm-secret']; + + if (hasSession) { + // Return full player data for authenticated requests + res.json(players); + } else { + // Only send name for dropdown if not authenticated + res.json(players.map(p => ({ name: p.name }))); } - res.json(players); } catch (error) { logToFile('API: Failed to fetch players', error); res.status(500).json({ error: 'Failed to fetch players' }); } }); -// Get a single player by name (require session) -router.get('/:name', requireSession, async (req, res) => { +// Get player by name +router.get('/:name', async (req, res) => { try { - const player = await Player.findOne({ name: req.params.name }); - logToFile('API: Fetch player', req.params.name, player); + const { name } = req.params; + console.log('Getting player:', name); + + const player = await playerHelpers.getByName(name); if (!player) { return res.status(404).json({ error: 'Player not found' }); } + + logToFile('API: Fetch player', name, 'success'); res.json(player); } catch (error) { - logToFile('API: Failed to fetch player', req.params.name, error); - res.status(500).json({ error: 'Failed to fetch player' }); + console.error('Get player error:', error); + logToFile('API: Failed to get player', req.params.name, error); + res.status(500).json({ error: String(error) }); } }); -// Helper to flatten tabInfo -function flattenTabInfo(tabInfo) { - let t = tabInfo; - while (t && t.tabInfo) t = t.tabInfo; - return { ...t }; -} +// Update player data +router.put('/:name', async (req, res) => { + try { + const { name } = req.params; + const playerData = req.body; + + console.log('Updating player:', name); + + const success = await playerHelpers.update(name, playerData); + if (!success) { + return res.status(500).json({ error: 'Failed to update player' }); + } + + logToFile('API: Updated player', name, 'success'); + res.json({ success: true, message: 'Player updated successfully' }); + } catch (error) { + console.error('Update player error:', error); + logToFile('API: Failed to update player', req.params.name, error); + res.status(500).json({ error: String(error) }); + } +}); -// Create a new player +// Create new player router.post('/', async (req, res) => { try { - logToFile('API: Creating player', req.body); - const body = { ...req.body }; - if (body.tabInfo) body.tabInfo = flattenTabInfo(body.tabInfo); - const newPlayer = new Player(body); - await newPlayer.save(); - logToFile('API: Player created', newPlayer); - res.status(201).json(newPlayer); + const playerData = req.body; + + console.log('Creating player:', playerData.name); + + // Check if player already exists + const existing = await playerHelpers.getByName(playerData.name); + if (existing) { + return res.status(409).json({ error: 'Player already exists' }); + } + + const playerId = await playerHelpers.create(playerData); + if (!playerId) { + return res.status(500).json({ error: 'Failed to create player' }); + } + + logToFile('API: Created player', playerData.name, 'success'); + res.json({ success: true, id: playerId, message: 'Player created successfully' }); } catch (error) { - logToFile('API: Failed to create player', error); - res.status(400).json({ error: 'Failed to create player' }); + console.error('Create player error:', error); + logToFile('API: Failed to create player', req.body.name, error); + res.status(500).json({ error: String(error) }); } }); -// Update a player (require session) -router.put('/:name', requireSession, async (req, res) => { +// Delete player +router.delete('/:name', async (req, res) => { try { - logToFile('API: Updating player', req.params.name, req.body); - // Always extract pw and pwHash from any location in the request - let pw, pwHash; - let tabInfo = req.body.tabInfo; - if (req.body.pw !== undefined) pw = req.body.pw; - if (req.body.pwHash !== undefined) pwHash = req.body.pwHash; - if (tabInfo !== undefined) { - if (tabInfo.pw !== undefined) pw = tabInfo.pw; - if (tabInfo.pwHash !== undefined) pwHash = tabInfo.pwHash; - if (tabInfo.tabInfo) { - if (tabInfo.tabInfo.pw !== undefined) pw = tabInfo.tabInfo.pw; - if (tabInfo.tabInfo.pwHash !== undefined) pwHash = tabInfo.tabInfo.pwHash; - } - tabInfo = flattenTabInfo(tabInfo); + const { name } = req.params; + + console.log('Deleting player:', name); + + const success = await playerHelpers.delete(name); + if (!success) { + return res.status(404).json({ error: 'Player not found or could not be deleted' }); } - const update = {}; - if (pw !== undefined) update.pw = pw; - if (pwHash !== undefined) update.pwHash = pwHash; - if (tabInfo !== undefined) update.tabInfo = tabInfo; - if (Object.keys(update).length === 0) { - logToFile('API: No valid fields to update for player', req.params.name); - return res.status(400).json({ error: 'No valid fields to update' }); - } - const updatedPlayer = await Player.findOneAndUpdate( - { name: req.params.name }, - update, - { new: true } - ); - if (!updatedPlayer) { - logToFile('API: Player not found for update', req.params.name); - return res.status(404).json({ error: 'Player not found' }); - } - logToFile('API: Player updated', updatedPlayer); - res.json(updatedPlayer); + + logToFile('API: Deleted player', name, 'success'); + res.json({ success: true, message: 'Player deleted successfully' }); } catch (error) { - logToFile('API: Failed to update player', error); - res.status(400).json({ error: 'Failed to update player' }); - } -}); - -// Delete a player (require session) -router.delete('/:name', requireSession, async (req, res) => { - try { - logToFile('API: Deleting player', req.params.name); - const deletedPlayer = await Player.findOneAndDelete({ name: req.params.name }); - if (!deletedPlayer) { - logToFile('API: Player not found for delete', req.params.name); - return res.status(404).json({ error: 'Player not found' }); - } - logToFile('API: Player deleted', req.params.name); - res.status(204).send(); - } catch (error) { - logToFile('API: Failed to delete player', error); - res.status(500).json({ error: 'Failed to delete player' }); + console.error('Delete player error:', error); + logToFile('API: Failed to delete player', req.params.name, error); + res.status(500).json({ error: String(error) }); } }); +console.log('Player routes registered (MariaDB)'); module.exports = router; diff --git a/database/routes/rulesRoutes.js b/database/routes/rulesRoutes.js index 2e8ada2..4ab6422 100644 --- a/database/routes/rulesRoutes.js +++ b/database/routes/rulesRoutes.js @@ -1,10 +1,10 @@ const express = require('express'); const fs = require('fs'); const path = require('path'); -const { db } = require('../sqlite-db'); +const { rulesHelpers, logToFile } = require('../mariadb'); const router = express.Router(); -console.log('Rules routes registered (sqlite-backed)'); +console.log('Rules routes registered (MariaDB)'); // Helpers to clean up OCR/extracted text function cleanText(s) { @@ -32,62 +32,95 @@ function cleanTitle(t) { return s; } -// We will query sqlite `rules` table on demand; helper to fetch all rules -function getAllRules() { +// Fetch all rules from MariaDB +async function getAllRules() { try { - const rows = db.prepare('SELECT id, rule_id, title, content, page, source, source_abbr, category FROM rules ORDER BY id').all(); - return rows.map(r => ({ 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 })); + 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 + })); } catch (e) { - console.error('Failed to read rules from sqlite:', e); + console.error('Failed to read rules from MariaDB:', e); + logToFile('Error getting all rules:', e); return []; } } -function getRuleById(ruleId) { + +async function getRuleById(ruleId) { try { - const row = db.prepare('SELECT rule_id as id, title, content, page, source, source_abbr as sourceAbbr, category FROM rules WHERE rule_id = ?').get(ruleId); - if (!row) return null; - return { id: row.id, title: cleanTitle(row.title), content: cleanText(row.content), page: row.page, source: row.source, sourceAbbr: row.sourceAbbr, category: row.category }; + const rows = await rulesHelpers.getAll(); + 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 + }; } catch (e) { console.error('Failed to read rule by id:', e); + logToFile('Error getting rule by id:', ruleId, e); return null; } } // Get all rule categories -router.get('/categories', (req, res) => { +router.get('/categories', async (req, res) => { try { - const rows = getAllRules(); - const categories = [...new Set(rows.map(r => r.category).filter(Boolean))]; - const categoryList = categories.map(cat => ({ id: cat, name: cleanTitle(cat) })); - res.json([{ id: 'all', name: 'All Rules' }, ...categoryList]); + const rows = await getAllRules(); + const categories = [...new Set(rows.map(r => r.category).filter(Boolean))]; + const categoryList = categories.map(cat => ({ id: cat, name: cleanTitle(cat) })); + res.json([{ id: 'all', name: 'All Rules' }, ...categoryList]); } catch (error) { console.error('Categories error:', error); res.status(500).json({ error: 'Failed to get categories' }); } }); -// Search rules (sqlite-backed) -router.get('/search', (req, res) => { +// Search rules (MariaDB) +router.get('/search', async (req, res) => { try { const { q: query, category, limit = 20 } = req.query; if (!query || !query.trim()) return res.json([]); const limitInt = Math.max(1, parseInt(limit) || 20); - const term = `%${query}%`; - - let rows; - if (category && category !== 'all') { - rows = db.prepare('SELECT rule_id as id, title, content, page, source, source_abbr as sourceAbbr, category FROM rules WHERE (title LIKE ? OR content LIKE ?) AND category = ? ORDER BY id LIMIT ?').all(term, term, category, limitInt); - if (!rows || rows.length === 0) { - rows = db.prepare('SELECT rule_id as id, title, content, page, source, source_abbr as sourceAbbr, category FROM rules WHERE (title LIKE ? OR content LIKE ?) ORDER BY id LIMIT ?').all(term, term, limitInt); - } - } else { - rows = db.prepare('SELECT rule_id as id, title, content, page, source, source_abbr as sourceAbbr, category FROM rules WHERE (title LIKE ? OR content LIKE ?) ORDER BY id LIMIT ?').all(term, term, limitInt); + + const allRules = await getAllRules(); + const term = query.toLowerCase(); + + let filtered = allRules.filter(rule => { + const titleMatch = rule.title && rule.title.toLowerCase().includes(term); + const contentMatch = rule.content && rule.content.toLowerCase().includes(term); + const categoryMatch = !category || category === 'all' || rule.category === category; + + return (titleMatch || contentMatch) && categoryMatch; + }); + + // If category filtering yielded no results, try without category filter + if (filtered.length === 0 && category && category !== 'all') { + filtered = allRules.filter(rule => { + const titleMatch = rule.title && rule.title.toLowerCase().includes(term); + const contentMatch = rule.content && rule.content.toLowerCase().includes(term); + return titleMatch || contentMatch; + }); } - - const results = (rows || []).map(r => ({ ...r, content: r.content && r.content.length > 300 ? r.content.substring(0,300) + '...' : r.content })); - // Clean text fields before returning - const cleaned = (results || []).map(r => ({ ...r, title: cleanTitle(r.title), content: r.content ? cleanText(r.content) : r.content })); - res.json(cleaned); + + // Limit results and truncate content + const results = filtered.slice(0, limitInt).map(r => ({ + ...r, + content: r.content && r.content.length > 300 ? r.content.substring(0, 300) + '...' : r.content + })); + + res.json(results); } catch (error) { console.error('Search error:', error); res.status(500).json({ error: 'Search failed' }); @@ -95,10 +128,10 @@ router.get('/search', (req, res) => { }); // Get a specific rule by ID -router.get('/rule/:id', (req, res) => { +router.get('/rule/:id', async (req, res) => { try { const { id } = req.params; - const rule = getRuleById(id); + const rule = await getRuleById(id); if (!rule) return res.status(404).json({ error: 'Rule not found' }); res.json(rule); } catch (error) { @@ -108,20 +141,26 @@ router.get('/rule/:id', (req, res) => { }); // Get random rules for discovery -router.get('/random', (req, res) => { +router.get('/random', async (req, res) => { try { const { count = 5, category } = req.query; const max = Math.max(1, parseInt(count) || 5); - let rows; + + const allRules = await getAllRules(); + let filtered = allRules; + if (category && category !== 'all') { - rows = db.prepare('SELECT rule_id as id, title, content, page, source, source_abbr as sourceAbbr, category FROM rules WHERE category = ? ORDER BY RANDOM() LIMIT ?').all(category, max); - } else { - rows = db.prepare('SELECT rule_id as id, title, content, page, source, source_abbr as sourceAbbr, category FROM rules ORDER BY RANDOM() LIMIT ?').all(max); + filtered = allRules.filter(rule => rule.category === category); } - const randomRules = (rows || []).map(r => ({ ...r, content: r.content && r.content.length > 200 ? r.content.substring(0,200) + '...' : r.content })); - // Clean before responding - const cleaned = (randomRules || []).map(r => ({ ...r, title: cleanTitle(r.title), content: r.content ? cleanText(r.content) : r.content })); - res.json(cleaned); + + // Shuffle and pick random rules + const shuffled = filtered.sort(() => 0.5 - Math.random()); + const randomRules = shuffled.slice(0, max).map(r => ({ + ...r, + content: r.content && r.content.length > 200 ? r.content.substring(0, 200) + '...' : r.content + })); + + res.json(randomRules); } catch (error) { console.error('Random rules error:', error); res.status(500).json({ error: 'Failed to get random rules' }); @@ -129,11 +168,26 @@ router.get('/random', (req, res) => { }); // Get rules statistics -router.get('/stats', (req, res) => { +router.get('/stats', async (req, res) => { try { - const totalRules = db.prepare('SELECT COUNT(*) as c FROM rules').get().c; - const categories = db.prepare('SELECT category, COUNT(*) as c FROM rules GROUP BY category').all(); - const sources = db.prepare('SELECT source, COUNT(*) as c FROM rules GROUP BY source').all(); + const allRules = await getAllRules(); + const totalRules = allRules.length; + + const categoryCount = {}; + const sourceCount = {}; + + allRules.forEach(rule => { + if (rule.category) { + categoryCount[rule.category] = (categoryCount[rule.category] || 0) + 1; + } + if (rule.source) { + sourceCount[rule.source] = (sourceCount[rule.source] || 0) + 1; + } + }); + + const categories = Object.entries(categoryCount).map(([category, c]) => ({ category, c })); + const sources = Object.entries(sourceCount).map(([source, c]) => ({ source, c })); + res.json({ totalRules, categories, sources, searchTerms: 0 }); } catch (error) { console.error('Stats error:', error); @@ -142,12 +196,15 @@ router.get('/stats', (req, res) => { }); // Reload the rules database (admin only) -router.post('/reload', (req, res) => { +router.post('/reload', async (req, res) => { try { const gmSecret = req.headers['x-gm-secret']; if (gmSecret !== 'bongo') return res.status(403).json({ error: 'Unauthorized' }); - const totalRules = db.prepare('SELECT COUNT(*) as c FROM rules').get().c; - res.json({ success: true, totalRules, message: 'Rules are sqlite-backed; no reload necessary' }); + + const allRules = await getAllRules(); + const totalRules = allRules.length; + + res.json({ success: true, totalRules, message: 'Rules are MariaDB-backed; already loaded' }); } catch (error) { console.error('Reload error:', error); res.status(500).json({ error: 'Failed to reload database' }); diff --git a/database/routes/rulesStagingRoutes.js b/database/routes/rulesStagingRoutes.js index 380d5b8..f888316 100644 --- a/database/routes/rulesStagingRoutes.js +++ b/database/routes/rulesStagingRoutes.js @@ -1,11 +1,11 @@ const express = require('express'); const router = express.Router(); -const { db, logToFile, stagingHelpers } = require('../sqlite-db'); +const { stagingHelpers, rulesHelpers, logToFile } = require('../mariadb'); // List staged sanitized rules -router.get('/', (req, res) => { +router.get('/', async (req, res) => { try { - const rows = stagingHelpers.list(); + const rows = await stagingHelpers.getAll(); res.json(rows); } catch (e) { logToFile('staging:list:error', e && e.message); @@ -14,18 +14,30 @@ router.get('/', (req, res) => { }); // Approve staged rules: insert into rules table (appends) and clear staging -router.post('/approve', (req, res) => { +router.post('/approve', async (req, res) => { try { - const rows = stagingHelpers.list(); - const insert = db.prepare(`INSERT INTO rules (rule_id,title,content,page,source,source_abbr,category,created_at) VALUES (?,?,?,?,?,?,?,datetime('now'))`); - const insertMany = db.transaction((items) => { - items.forEach((it) => { - insert.run(null, it.title || '', it.content || '', it.page || '', 'sanitized', 'SAN', it.category || null); - }); - }); - insertMany(rows); - stagingHelpers.clear(); - res.json({ success: true, inserted: rows.length }); + const rows = await stagingHelpers.getAll(); + let inserted = 0; + + for (const rule of rows) { + const ruleData = { + title: rule.title || '', + content: rule.content || '', + page_num: rule.page || '', + source: 'sanitized', + source_abbr: 'SAN', + category: rule.category || null, + rulebook: 'staging' + }; + + const result = await rulesHelpers.create(ruleData); + if (result) { + inserted++; + await stagingHelpers.delete(rule.id); + } + } + + res.json({ success: true, inserted }); } catch (e) { logToFile('staging:approve:error', e && e.stack ? e.stack : e); res.status(500).json({ error: e.message }); @@ -33,9 +45,12 @@ router.post('/approve', (req, res) => { }); // Clear staging without approving -router.delete('/', (req, res) => { +router.delete('/', async (req, res) => { try { - const del = stagingHelpers.clear(); + const rows = await stagingHelpers.getAll(); + for (const rule of rows) { + await stagingHelpers.delete(rule.id); + } res.json({ success: true }); } catch (e) { logToFile('staging:clear:error', e && e.message); diff --git a/database/routes/sessionRoutes-sqlite.js b/database/routes/sessionRoutes-sqlite.js deleted file mode 100644 index ffbb79c..0000000 --- a/database/routes/sessionRoutes-sqlite.js +++ /dev/null @@ -1,111 +0,0 @@ -const express = require('express'); -const { sessionHelpers, playerHelpers } = require('../sqlite-db'); -const crypto = require('crypto'); -const fs = require('fs'); -const path = require('path'); - -// Simple file logger -function logToFile(...args) { - const msg = `[${new Date().toISOString()}] ` + args.map(a => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ') + '\n'; - fs.appendFileSync(path.join(__dirname, '../backend.log'), msg, { encoding: 'utf8' }); -} - -const router = express.Router(); - -// Create a new session (login) -router.post('/login', async (req, res) => { - try { - const { playerName, username, password } = req.body; - const name = playerName || username; // Accept either field name - - if (!name) { - logToFile('SESSION: Login missing playerName/username'); - return res.status(400).json({ error: 'playerName or username required' }); - } - - // Check if player exists - const player = playerHelpers.getByName(name); - if (!player) { - logToFile('SESSION: Login player not found', name); - return res.status(401).json({ error: 'Invalid credentials' }); - } - - // Check password if provided - if (password) { - // For now, compare with plain text (you can add bcrypt later) - const playerPw = player.pw || ''; - if (playerPw !== password) { - logToFile('SESSION: Login invalid password', name); - return res.status(401).json({ error: 'Invalid credentials' }); - } - } - - // Create session - const sessionId = crypto.randomBytes(32).toString('hex'); - const expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(); // 24h - - sessionHelpers.create(sessionId, { playerName: name }, expiresAt); - - logToFile('SESSION: Login success', name, sessionId); - res.json({ sessionId, expiresAt, playerName: name, success: true }); - } catch (error) { - logToFile('SESSION: Login error', error); - res.status(500).json({ error: 'Login failed' }); - } -}); - -// Validate session -router.post('/validate', async (req, res) => { - try { - const { sessionId } = req.body; - - if (!sessionId) { - logToFile('SESSION: Validate missing sessionId'); - return res.status(400).json({ error: 'sessionId required' }); - } - - const session = sessionHelpers.get(sessionId); - - if (!session) { - logToFile('SESSION: Validate not found', sessionId); - return res.status(401).json({ error: 'Invalid or expired session' }); - } - - logToFile('SESSION: Validate success', sessionId, session.data.playerName); - res.json({ valid: true, playerName: session.data.playerName }); - } catch (error) { - logToFile('SESSION: Validate error', error); - res.status(500).json({ error: 'Validation failed' }); - } -}); - -// Logout (delete session) -router.post('/logout', async (req, res) => { - try { - const { sessionId } = req.body; - - if (sessionId) { - sessionHelpers.delete(sessionId); - logToFile('SESSION: Logout success', sessionId); - } - - res.json({ success: true }); - } catch (error) { - logToFile('SESSION: Logout error', error); - res.status(500).json({ error: 'Logout failed' }); - } -}); - -// Clean expired sessions (can be called periodically) -router.post('/cleanup', async (req, res) => { - try { - const deletedCount = sessionHelpers.cleanExpired(); - logToFile('SESSION: Cleanup completed', `${deletedCount} sessions removed`); - res.json({ deletedCount }); - } catch (error) { - logToFile('SESSION: Cleanup error', error); - res.status(500).json({ error: 'Cleanup failed' }); - } -}); - -module.exports = router; diff --git a/database/routes/sessionRoutes.js b/database/routes/sessionRoutes.js index 6b351a2..274d082 100644 --- a/database/routes/sessionRoutes.js +++ b/database/routes/sessionRoutes.js @@ -1,50 +1,37 @@ const express = require('express'); -const Session = require('../sessionModel'); +const { logToFile } = require('../mariadb'); const router = express.Router(); -const crypto = require('crypto'); -// Create a new session (login) -router.post('/login', async (req, res) => { - const { playerName } = req.body; - if (!playerName) return res.status(400).json({ error: 'playerName required' }); - const sessionId = crypto.randomBytes(32).toString('hex'); - const expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24); // 24h - const session = new Session({ sessionId, playerName, expiresAt }); - await session.save(); - res.json({ sessionId, expiresAt }); -}); - -// Validate session -const fs = require('fs'); -const path = require('path'); -function logToFile(...args) { - const msg = `[${new Date().toISOString()}] ` + args.map(a => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ') + '\n'; - fs.appendFileSync(path.join(__dirname, '../backend.log'), msg, { encoding: 'utf8' }); -} +// Simple session validation endpoint router.post('/validate', async (req, res) => { - const { sessionId } = req.body; - if (!sessionId) { - logToFile('SESSION: Validate missing sessionId'); - return res.status(400).json({ error: 'sessionId required' }); + try { + const { sessionId } = req.body; + + if (!sessionId) { + logToFile('SESSION: Validate missing sessionId'); + return res.status(400).json({ error: 'sessionId required' }); + } + + // Extract player name from session ID (simple format: session_playername_timestamp) + const match = sessionId.match(/^session_([^_]+)_\d+$/); + if (!match) { + logToFile('SESSION: Invalid session format', sessionId); + return res.status(401).json({ error: 'Invalid session format' }); + } + + const playerName = match[1]; + + logToFile('SESSION: Session validation successful', playerName); + res.json({ + valid: true, + playerName + }); + } catch (error) { + console.error('Session validation error:', error); + logToFile('SESSION: Failed to validate session', error); + res.status(500).json({ error: String(error) }); } - const session = await Session.findOne({ sessionId }); - if (!session) { - logToFile('SESSION: Validate not found', sessionId); - return res.status(401).json({ error: 'Invalid or expired session' }); - } - if (session.expiresAt < new Date()) { - logToFile('SESSION: Validate expired', sessionId); - return res.status(401).json({ error: 'Invalid or expired session' }); - } - logToFile('SESSION: Validate success', sessionId, session.playerName); - res.json({ valid: true, playerName: session.playerName }); -}); - -// Logout (delete session) -router.post('/logout', async (req, res) => { - const { sessionId } = req.body; - await Session.deleteOne({ sessionId }); - res.json({ success: true }); }); +console.log('Session routes registered (simple validation)'); module.exports = router; diff --git a/database/routes/shopRoutes.js b/database/routes/shopRoutes.js index e429179..b7b20af 100644 --- a/database/routes/shopRoutes.js +++ b/database/routes/shopRoutes.js @@ -2,6 +2,15 @@ const express = require('express'); const fs = require('fs'); const path = require('path'); const router = express.Router(); +const { playerHelpers } = require('../mariadb'); // Use MariaDB instead of SQLite + +console.log('ShopRoutes: Loading with purchase endpoint (MariaDB)'); + +// Simple file logger +function logToFile(...args) { + const msg = `[${new Date().toISOString()}] ` + args.map(a => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ') + '\n'; + fs.appendFileSync(path.join(__dirname, '../backend.log'), msg, { encoding: 'utf8' }); +} // Minimal shop index - attempt to serve a generated armoury/shop JSON if present router.get('/', (req, res) => { @@ -18,4 +27,125 @@ router.get('/', (req, res) => { } }); +// Test route to verify routes are working +router.get('/test', (req, res) => { + res.json({ message: 'Shop routes are working' }); +}); + +// Purchase item endpoint +router.post('/purchase', async (req, res) => { + console.log('ShopRoutes: Purchase endpoint hit!'); + try { + const { playerId, itemId, quantity = 1 } = req.body; + const sessionId = req.headers['x-session-id']; + + if (!sessionId) { + return res.status(401).json({ error: 'Session required' }); + } + + if (!playerId || !itemId) { + return res.status(400).json({ error: 'Player ID and Item ID required' }); + } + + // Get the player + const players = await playerHelpers.getAll(); + const player = players.find(p => p.id === playerId || p.name === playerId); + + if (!player) { + return res.status(404).json({ error: 'Player not found' }); + } + + // Get shop data to find the item + const shopPath = path.join(__dirname, '..', '..', 'public', 'deathwatch-armoury.json'); + if (!fs.existsSync(shopPath)) { + return res.status(500).json({ error: 'Shop data not available' }); + } + + const shopData = JSON.parse(fs.readFileSync(shopPath, 'utf8')); + let foundItem = null; + + // Search for the item in all categories + for (const category in shopData.items) { + if (shopData.items[category]) { + foundItem = shopData.items[category].find(item => + (item.id === itemId) || (`${category}-${item.name}` === itemId) + ); + if (foundItem) break; + } + } + + if (!foundItem) { + return res.status(404).json({ error: 'Item not found in shop' }); + } + + // Check if item has cost > 0 (purchasable) + const itemCost = foundItem.req || 0; + if (itemCost <= 0) { + return res.status(400).json({ error: 'This item is not purchasable' }); + } + + // Check player RP + const playerRp = player.tabInfo?.rp || 0; + const totalCost = itemCost * quantity; + + if (playerRp < totalCost) { + return res.status(400).json({ + error: `Insufficient Requisition Points. Need ${totalCost} RP but only have ${playerRp} RP.` + }); + } + + // Add item to player gear and deduct RP + const updatedTabInfo = { ...player.tabInfo }; + updatedTabInfo.rp = playerRp - totalCost; + + // Initialize gear if it doesn't exist + if (!updatedTabInfo.gear) { + updatedTabInfo.gear = []; + } + + // Create gear item in the format expected by character sheet + const gearItem = { + name: foundItem.name, + qty: quantity + }; + + // Check if item already exists in gear + const existingGearIndex = updatedTabInfo.gear.findIndex(gearItem => + gearItem.name === foundItem.name + ); + + if (existingGearIndex >= 0) { + // Update existing item quantity + updatedTabInfo.gear[existingGearIndex].qty += quantity; + } else { + // Add new item to gear + updatedTabInfo.gear.push(gearItem); + } + + // Update player data + const success = await playerHelpers.update(player.name, { + ...player, + tabInfo: updatedTabInfo + }); + + if (!success) { + return res.status(500).json({ error: 'Failed to update player data' }); + } + + logToFile(`Purchase: ${player.name} bought ${quantity}x ${foundItem.name} for ${totalCost} RP`); + + res.json({ + success: true, + message: `Successfully purchased ${quantity}x ${foundItem.name} for ${totalCost} RP`, + newRp: updatedTabInfo.rp, + item: gearItem + }); + + } catch (error) { + console.error('Purchase error:', error); + logToFile('API: Purchase failed', error); + res.status(500).json({ error: 'Purchase failed' }); + } +}); + module.exports = router; diff --git a/database/routes/weaponsRoutes.js b/database/routes/weaponsRoutes.js index 6aa4898..8ddd3c5 100644 --- a/database/routes/weaponsRoutes.js +++ b/database/routes/weaponsRoutes.js @@ -1,19 +1,24 @@ const express = require('express') const router = express.Router() -const { db } = require('../sqlite-db') +const { weaponsHelpers, logToFile } = require('../mariadb') // Return weapons in a normalized shape -router.get('/', (req, res) => { +router.get('/', async (req, res) => { try { - const rows = db.prepare('SELECT id,name,category,stats,source FROM weapons ORDER BY name').all() + const rows = await weaponsHelpers.getAll() const parsed = rows.map(r => { let stats = {} - try { stats = JSON.parse(r.stats || '{}') } catch (e) {} + try { + stats = typeof r.stats === 'string' ? JSON.parse(r.stats) : r.stats || {} + } catch (e) { + logToFile('Error parsing weapon stats for', r.name, e) + } return { id: r.id, name: r.name, category: r.category, stats: stats, source: r.source } }) res.json(parsed) } catch (e) { - console.error('Failed to load weapons from sqlite:', e) + console.error('Failed to load weapons from MariaDB:', e) + logToFile('Error getting weapons:', e) res.status(500).json({ error: 'Failed to get weapons' }) } }) diff --git a/database/server-sqlite.js b/database/server-sqlite.js deleted file mode 100644 index 841ae0c..0000000 --- a/database/server-sqlite.js +++ /dev/null @@ -1,64 +0,0 @@ -require('dotenv').config(); -const express = require('express'); -const cors = require('cors'); -const fs = require('fs'); -const path = require('path'); -const playerRoutes = require('./routes/playerRoutes-sqlite'); -const sessionRoutes = require('./routes/sessionRoutes-sqlite'); - -const app = express(); -const PORT = process.env.PORT || 5000; - -// Middleware -app.use(express.json()); -app.use(cors()); -app.use(express.static('public')); // Serve files from public directory - -// Initialize SQLite database -const { db } = require('./sqlite-db'); - -// Graceful shutdown -process.on('SIGINT', () => { - console.log('Closing SQLite database...'); - db.close(); - process.exit(0); -}); - -process.on('SIGTERM', () => { - console.log('Closing SQLite database...'); - db.close(); - process.exit(0); -}); - -// Root route for friendly message -app.get('/', (req, res) => { - res.send('Deathwatch Roller API is running with SQLite. Use /api/players for player data.'); -}); - -// Shop endpoint -app.get('/api/shop', (req, res) => { - try { - console.log('Shop endpoint hit'); - const filepath = path.join(__dirname, '../public/deathwatch-armoury.json'); - console.log('Looking for shop data at:', filepath); - console.log('File exists:', fs.existsSync(filepath)); - const shopData = JSON.parse(fs.readFileSync(filepath, 'utf8')); - console.log('Shop data loaded, keys:', Object.keys(shopData)); - res.json(shopData); - } catch (error) { - console.error('Shop error:', error); - res.status(500).json({ error: String(error) }); - } -}); - -// Use routes -app.use('/api/players', playerRoutes); -app.use('/api/sessions', sessionRoutes); - -// Start Server -app.listen(PORT, '0.0.0.0', () => { - console.log(`Server running on http://0.0.0.0:${PORT}`); - console.log('Using SQLite database'); -}); - -module.exports = app; diff --git a/database/server.js b/database/server.js index 7005742..b4c7fe5 100755 --- a/database/server.js +++ b/database/server.js @@ -3,8 +3,9 @@ const express = require('express'); const cors = require('cors'); const fs = require('fs'); const path = require('path'); -const playerRoutes = require('./routes/playerRoutes-sqlite'); -const sessionRoutes = require('./routes/sessionRoutes-sqlite'); +// MariaDB routes +const playerRoutes = require('./routes/playerRoutes'); +const sessionRoutes = require('./routes/sessionRoutes'); const shopRoutes = require('./routes/shopRoutes'); const rulesRoutes = require('./routes/rulesRoutes'); const bestiaryRoutes = require('./routes/bestiaryRoutes'); @@ -14,12 +15,14 @@ const rulesStagingRoutes = require('./routes/rulesStagingRoutes'); const gmkitDir = path.join(__dirname, '..', 'data', 'gamemasters_kit'); +// Initialize MariaDB +require('./mariadb'); + console.log('Routes loaded:', { - playerRoutes: typeof playerRoutes, - sessionRoutes: typeof sessionRoutes, shopRoutes: typeof shopRoutes, rulesRoutes: typeof rulesRoutes, - bestiaryRoutes: typeof bestiaryRoutes + bestiaryRoutes: typeof bestiaryRoutes, + weaponsRoutes: typeof weaponsRoutes }); const app = express(); @@ -31,6 +34,7 @@ app.use(cors()); // API Routes (before static files) console.log('Registering API routes...'); +// Player routes now working with MariaDB try { console.log('Registering /api/players'); app.use('/api/players', playerRoutes); @@ -62,7 +66,25 @@ try { } catch (e) { console.error('Error mounting /api/rules:', e && e.stack ? e.stack : e); throw e; -} + } + + try { + console.log('Registering /api/weapons'); + app.use('/api/weapons', weaponsRoutes); + console.log('Weapons routes registered'); + } catch (e) { + console.error('Error mounting /api/weapons:', e && e.stack ? e.stack : e); + throw e; + } + + try { + console.log('Registering /api/bestiary'); + app.use('/api/bestiary', bestiaryRoutes); + console.log('Bestiary routes registered'); + } catch (e) { + console.error('Error mounting /api/bestiary:', e && e.stack ? e.stack : e); + throw e; + } try { console.log('Registering /api/rules/staging'); @@ -70,9 +92,7 @@ try { console.log('Rules staging routes registered'); } catch (e) { console.error('Error mounting /api/rules/staging:', e && e.stack ? e.stack : e); - } - - // Expose gamemaster kit files and a simple listing API for GM-only resources + } // Expose gamemaster kit files and a simple listing API for GM-only resources try { console.log('Registering /api/gmkit and /gmkit static'); app.get('/api/gmkit/list', (req, res) => { @@ -157,19 +177,14 @@ app.use('/avatars', express.static(avatarsDir)); // res.sendFile(path.join(buildDir, 'index.html')); // }); -// Initialize SQLite database -const { db } = require('./sqlite-db'); - -// Graceful shutdown +// Graceful shutdown - MariaDB connections are handled by the pool process.on('SIGINT', () => { - console.log('Closing SQLite database...'); - db.close(); + console.log('Shutting down server...'); process.exit(0); }); process.on('SIGTERM', () => { - console.log('Closing SQLite database...'); - db.close(); + console.log('Shutting down server...'); process.exit(0); }); @@ -188,34 +203,16 @@ if (fs.existsSync(indexHtml)) { } else { // Root route for friendly message when no build is present app.get('/', (req, res) => { - res.send('Deathwatch Roller API is running with SQLite. Use /api/players for player data.'); + res.send('Deathwatch Roller API is running with MariaDB. Use /api/shop for shop data.'); }); } -// Use routes (instrument mounts to debug invalid route patterns) -console.log('Mounting route: /api/shop'); -app.use('/api/shop', shopRoutes); -console.log('Mounted /api/shop'); -console.log('Mounting route: /api/players'); -app.use('/api/players', playerRoutes); -console.log('Mounted /api/players'); -console.log('Mounting route: /api/sessions'); -app.use('/api/sessions', sessionRoutes); -console.log('Mounted /api/sessions'); -console.log('Mounting route: /api/rules'); -app.use('/api/rules', rulesRoutes); -console.log('Mounted /api/rules'); -console.log('Mounting route: /api/bestiary'); -app.use('/api/bestiary', bestiaryRoutes); -console.log('Mounted /api/bestiary'); -console.log('Mounting route: /api/weapons'); -app.use('/api/weapons', weaponsRoutes); -console.log('Mounted /api/weapons'); +// Routes have already been registered above - no need to duplicate // Start Server app.listen(PORT, '0.0.0.0', () => { console.log(`Server running on http://0.0.0.0:${PORT}`); - console.log('Using SQLite database'); + console.log('Using MariaDB database'); }); module.exports = app; diff --git a/database/sqlite-db.db b/database/sqlite-db.db deleted file mode 100644 index e69de29..0000000 diff --git a/database/sqlite-db.js b/database/sqlite-db.js deleted file mode 100644 index 96a96a4..0000000 --- a/database/sqlite-db.js +++ /dev/null @@ -1,272 +0,0 @@ -const Database = require('better-sqlite3'); -const path = require('path'); -const fs = require('fs'); - -// Simple DB logger to backend.log -const backendLogPath = path.join(__dirname, 'backend.log'); -function logToFile(...args) { - try { - const msg = `[${new Date().toISOString()}] ` + args.map(a => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ') + '\n'; - fs.appendFileSync(backendLogPath, msg, { encoding: 'utf8' }); - } catch (err) { - console.error('Failed to write backend log', err); - } -} - -// Create database directory if it doesn't exist -const dbDir = path.join(__dirname, 'sqlite'); -if (!fs.existsSync(dbDir)) { - fs.mkdirSync(dbDir, { recursive: true }); -} - -const dbPath = path.join(dbDir, 'deathwatch.db'); -const db = new Database(dbPath); - -// Enable WAL mode for better concurrency -db.pragma('journal_mode = WAL'); - -// Create tables -const createTables = () => { - // Players table - db.exec(` - CREATE TABLE IF NOT EXISTS players ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT UNIQUE NOT NULL, - roller_info TEXT DEFAULT '{}', - shop_info TEXT DEFAULT '{}', - tab_info TEXT DEFAULT '{}', - pw TEXT DEFAULT '', - pw_hash TEXT DEFAULT '', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP - ) - `); - - // Sessions table (if you need it) - db.exec(` - CREATE TABLE IF NOT EXISTS sessions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT UNIQUE NOT NULL, - data TEXT DEFAULT '{}', - expires_at DATETIME, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP - ) - `); - - // Create indexes for better performance - db.exec(` - CREATE INDEX IF NOT EXISTS idx_players_name ON players(name); - CREATE INDEX IF NOT EXISTS idx_sessions_session_id ON sessions(session_id); - CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at); - CREATE TABLE IF NOT EXISTS rules_staging ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - title TEXT, - content TEXT, - category TEXT, - page TEXT, - original_json TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - ); - CREATE INDEX IF NOT EXISTS idx_rules_staging_category ON rules_staging(category); - `); - - console.log('SQLite tables created successfully'); -}; - -// Initialize database -createTables(); - -// Prepared statements for common operations -const statements = { - // Player operations - getAllPlayers: db.prepare('SELECT * FROM players ORDER BY name'), - getPlayerByName: db.prepare('SELECT * FROM players WHERE name = ?'), - insertPlayer: db.prepare(` - INSERT INTO players (name, roller_info, shop_info, tab_info, pw, pw_hash) - VALUES (?, ?, ?, ?, ?, ?) - `), - updatePlayer: db.prepare(` - UPDATE players - SET roller_info = ?, shop_info = ?, tab_info = ?, pw = ?, pw_hash = ?, updated_at = CURRENT_TIMESTAMP - WHERE name = ? - `), - deletePlayer: db.prepare('DELETE FROM players WHERE name = ?'), - - // Session operations - getSession: db.prepare('SELECT * FROM sessions WHERE session_id = ? AND expires_at > datetime(\'now\')'), - insertSession: db.prepare(` - INSERT INTO sessions (session_id, data, expires_at) - VALUES (?, ?, ?) - `), - updateSession: db.prepare(` - UPDATE sessions - SET data = ?, expires_at = ?, updated_at = CURRENT_TIMESTAMP - WHERE session_id = ? - `), - deleteSession: db.prepare('DELETE FROM sessions WHERE session_id = ?'), - cleanExpiredSessions: db.prepare('DELETE FROM sessions WHERE expires_at <= datetime(\'now\')') -}; - -// Helper functions -const playerHelpers = { - getAll: () => { - logToFile('DB: getAllPlayers - start'); - const rows = statements.getAllPlayers.all(); - const result = rows.map(row => ({ - name: row.name, - rollerInfo: JSON.parse(row.roller_info || '{}'), - shopInfo: JSON.parse(row.shop_info || '{}'), - tabInfo: JSON.parse(row.tab_info || '{}'), - pw: row.pw, - pwHash: row.pw_hash, - _id: row.id, - createdAt: row.created_at, - updatedAt: row.updated_at - })); - logToFile('DB: getAllPlayers - resultCount', result.length); - return result; - }, - - getByName: (name) => { - logToFile('DB: getPlayerByName - start', name); - const row = statements.getPlayerByName.get(name); - if (!row) { - logToFile('DB: getPlayerByName - not found', name); - return null; - } - const result = { - name: row.name, - rollerInfo: JSON.parse(row.roller_info || '{}'), - shopInfo: JSON.parse(row.shop_info || '{}'), - tabInfo: JSON.parse(row.tab_info || '{}'), - pw: row.pw, - pwHash: row.pw_hash, - _id: row.id, - createdAt: row.created_at, - updatedAt: row.updated_at - }; - logToFile('DB: getPlayerByName - found', name); - return result; - }, - - create: (playerData) => { - logToFile('DB: createPlayer - start', playerData.name); - const result = statements.insertPlayer.run( - playerData.name, - JSON.stringify(playerData.rollerInfo || {}), - JSON.stringify(playerData.shopInfo || {}), - JSON.stringify(playerData.tabInfo || {}), - playerData.pw || '', - playerData.pwHash || '' - ); - const out = { ...playerData, _id: result.lastInsertRowid }; - logToFile('DB: createPlayer - done', playerData.name, 'rowid', result.lastInsertRowid); - return out; - }, - - update: (name, playerData) => { - logToFile('DB: updatePlayer - start', name); - // Ensure we don't have nested tabInfo - const cleanTabInfo = playerData.tabInfo || {}; - if (cleanTabInfo.tabInfo) { - logToFile('DB: updatePlayer - fixing nested tabInfo structure'); - Object.assign(cleanTabInfo, cleanTabInfo.tabInfo); - delete cleanTabInfo.tabInfo; - } - const result = statements.updatePlayer.run( - JSON.stringify(playerData.rollerInfo || {}), - JSON.stringify(playerData.shopInfo || {}), - JSON.stringify(cleanTabInfo), - playerData.pw || '', - playerData.pwHash || '', - name - ); - logToFile('DB: updatePlayer - changes', result.changes, name); - return result.changes > 0; - }, - - delete: (name) => { - logToFile('DB: deletePlayer - start', name); - const result = statements.deletePlayer.run(name); - logToFile('DB: deletePlayer - changes', result.changes, name); - return result.changes > 0; - } -}; - -const sessionHelpers = { - get: (sessionId) => { - logToFile('DB: getSession - start', sessionId); - const row = statements.getSession.get(sessionId); - if (!row) { - logToFile('DB: getSession - not found', sessionId); - return null; - } - const out = { - sessionId: row.session_id, - data: JSON.parse(row.data || '{}'), - expiresAt: row.expires_at - }; - logToFile('DB: getSession - ok', sessionId); - return out; - }, - - create: (sessionId, data, expiresAt) => { - logToFile('DB: createSession - start', sessionId, 'expiresAt', expiresAt); - const res = statements.insertSession.run(sessionId, JSON.stringify(data), expiresAt); - logToFile('DB: createSession - done', sessionId); - }, - - update: (sessionId, data, expiresAt) => { - logToFile('DB: updateSession - start', sessionId); - const result = statements.updateSession.run(JSON.stringify(data), expiresAt, sessionId); - logToFile('DB: updateSession - changes', result.changes, sessionId); - return result.changes > 0; - }, - - delete: (sessionId) => { - logToFile('DB: deleteSession - start', sessionId); - const result = statements.deleteSession.run(sessionId); - logToFile('DB: deleteSession - changes', result.changes, sessionId); - return result.changes > 0; - }, - - cleanExpired: () => { - logToFile('DB: cleanExpiredSessions - start'); - const result = statements.cleanExpiredSessions.run(); - logToFile('DB: cleanExpiredSessions - removed', result.changes); - return result.changes; - } -}; - -// Staging helpers for sanitized rules -const stagingStatements = { - insertStaging: db.prepare('INSERT INTO rules_staging (title, content, category, page, original_json) VALUES (?,?,?,?,?)'), - listStaging: db.prepare('SELECT id, title, content, category, page, original_json, created_at FROM rules_staging ORDER BY id'), - deleteStagingAll: db.prepare('DELETE FROM rules_staging'), - getStaging: db.prepare('SELECT id, title, content, category, page, original_json, created_at FROM rules_staging WHERE id = ?') -}; - -const stagingHelpers = { - insert: (obj) => { - const res = stagingStatements.insertStaging.run(obj.title || '', obj.content || '', obj.category || '', obj.page || '', JSON.stringify(obj.original || {})); - return res.lastInsertRowid; - }, - list: () => stagingStatements.listStaging.all().map(r => ({ ...r, original: JSON.parse(r.original_json || '{}') })), - clear: () => stagingStatements.deleteStagingAll.run(), - get: (id) => { - const row = stagingStatements.getStaging.get(id); - if (!row) return null; - return { ...row, original: JSON.parse(row.original_json || '{}') }; - } -}; - -module.exports = { - db, - statements, - playerHelpers, - sessionHelpers, - stagingHelpers, - close: () => db.close(), - logToFile -}; diff --git a/database/sqlite/deathwatch.db b/database/sqlite/deathwatch.db deleted file mode 100644 index 21c3082..0000000 Binary files a/database/sqlite/deathwatch.db and /dev/null differ diff --git a/database/sqlite/deathwatch.db.backup.1755447418502 b/database/sqlite/deathwatch.db.backup.1755447418502 deleted file mode 100644 index e399776..0000000 Binary files a/database/sqlite/deathwatch.db.backup.1755447418502 and /dev/null differ diff --git a/database/sqlite/deathwatch.db.backup.1755449835460 b/database/sqlite/deathwatch.db.backup.1755449835460 deleted file mode 100644 index afdec48..0000000 Binary files a/database/sqlite/deathwatch.db.backup.1755449835460 and /dev/null differ diff --git a/database/sqlite/deathwatch.db.bak.20250816T093608 b/database/sqlite/deathwatch.db.bak.20250816T093608 deleted file mode 100644 index 9a2eb11..0000000 Binary files a/database/sqlite/deathwatch.db.bak.20250816T093608 and /dev/null differ diff --git a/database/sqlite/deathwatch.db.bak.20250816T093616 b/database/sqlite/deathwatch.db.bak.20250816T093616 deleted file mode 100644 index 9a2eb11..0000000 Binary files a/database/sqlite/deathwatch.db.bak.20250816T093616 and /dev/null differ diff --git a/database/sqlite/deathwatch.db.bak.20250816T093626 b/database/sqlite/deathwatch.db.bak.20250816T093626 deleted file mode 100644 index 9a2eb11..0000000 Binary files a/database/sqlite/deathwatch.db.bak.20250816T093626 and /dev/null differ diff --git a/database/sqlite/deathwatch.db.pre_apply_templates.2025-08-16T075151044Z.bak b/database/sqlite/deathwatch.db.pre_apply_templates.2025-08-16T075151044Z.bak deleted file mode 100644 index 9a2eb11..0000000 Binary files a/database/sqlite/deathwatch.db.pre_apply_templates.2025-08-16T075151044Z.bak and /dev/null differ diff --git a/database/sqlite/deathwatch.db.pre_delete_tests.2025-08-16T074318436Z.bak b/database/sqlite/deathwatch.db.pre_delete_tests.2025-08-16T074318436Z.bak deleted file mode 100644 index 9a2eb11..0000000 Binary files a/database/sqlite/deathwatch.db.pre_delete_tests.2025-08-16T074318436Z.bak and /dev/null differ diff --git a/database/sqlite/deathwatch.db.pre_migrate.2025-08-16T080212929Z.bak b/database/sqlite/deathwatch.db.pre_migrate.2025-08-16T080212929Z.bak deleted file mode 100644 index 9a2eb11..0000000 Binary files a/database/sqlite/deathwatch.db.pre_migrate.2025-08-16T080212929Z.bak and /dev/null differ diff --git a/database/sqlite/deathwatch.db.pre_normalize.2025-08-16T073920682Z.bak b/database/sqlite/deathwatch.db.pre_normalize.2025-08-16T073920682Z.bak deleted file mode 100644 index 9a2eb11..0000000 Binary files a/database/sqlite/deathwatch.db.pre_normalize.2025-08-16T073920682Z.bak and /dev/null differ diff --git a/database/sqlite/deathwatch.db.pre_transform.2025-08-16T081053187Z.bak b/database/sqlite/deathwatch.db.pre_transform.2025-08-16T081053187Z.bak deleted file mode 100644 index 9a2eb11..0000000 Binary files a/database/sqlite/deathwatch.db.pre_transform.2025-08-16T081053187Z.bak and /dev/null differ diff --git a/database/sqlite/deathwatch.db.pre_transform.2025-08-16T081219333Z.bak b/database/sqlite/deathwatch.db.pre_transform.2025-08-16T081219333Z.bak deleted file mode 100644 index 9a2eb11..0000000 Binary files a/database/sqlite/deathwatch.db.pre_transform.2025-08-16T081219333Z.bak and /dev/null differ diff --git a/database/sqlite/deathwatch.db.pre_transform.2025-08-16T081324978Z.bak b/database/sqlite/deathwatch.db.pre_transform.2025-08-16T081324978Z.bak deleted file mode 100644 index 9a2eb11..0000000 Binary files a/database/sqlite/deathwatch.db.pre_transform.2025-08-16T081324978Z.bak and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 887fad5..d325f5f 100755 --- a/package-lock.json +++ b/package-lock.json @@ -12,14 +12,14 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "bcrypt": "^5.1.0", - "better-sqlite3": "^12.2.0", "cors": "^2.8.5", "express": "^5.1.0", + "http-proxy-middleware": "^3.0.5", + "mysql2": "^3.14.4", "pdf-parse": "^1.1.1", "pdfjs-dist": "^3.9.179", "react": "^18.2.0", "react-dom": "^18.2.0", - "sqlite3": "^5.1.7", "web-vitals": "^2.1.4" }, "devDependencies": { @@ -2564,12 +2564,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", - "optional": true - }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -4223,54 +4217,6 @@ "node": ">= 8" } }, - "node_modules/@npmcli/fs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", - "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", - "optional": true, - "dependencies": { - "@gar/promisify": "^1.0.1", - "semver": "^7.3.5" - } - }, - "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "optional": true, - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@npmcli/move-file/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "optional": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -5063,7 +5009,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -5261,7 +5207,6 @@ "version": "1.17.16", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -6153,31 +6098,6 @@ "node": ">= 6.0.0" } }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "optional": true, - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "optional": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -6746,6 +6666,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/axe-core": { "version": "4.10.3", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", @@ -7081,25 +7010,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/basic-ftp": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", @@ -7129,19 +7039,6 @@ "node": ">= 10.0.0" } }, - "node_modules/better-sqlite3": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.2.0.tgz", - "integrity": "sha512-eGbYq2CT+tos1fBwLQ/tkBt9J5M3JEHjku4hbvQUePCckkvVf14xWj+1m7dGoK81M/fOjFT7yM9UMeKT/+vFLQ==", - "hasInstallScript": true, - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - }, - "engines": { - "node": "20.x || 22.x || 23.x || 24.x" - } - }, "node_modules/bfj": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.1.0.tgz", @@ -7182,24 +7079,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, "node_modules/blessed": { "version": "0.1.81", "resolved": "https://registry.npmjs.org/blessed/-/blessed-0.1.81.tgz", @@ -7341,29 +7220,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -7409,86 +7265,6 @@ "node": ">= 0.8" } }, - "node_modules/cacache": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", - "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", - "optional": true, - "dependencies": { - "@npmcli/fs": "^1.0.0", - "@npmcli/move-file": "^1.0.1", - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "infer-owner": "^1.0.4", - "lru-cache": "^6.0.0", - "minipass": "^3.1.1", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^1.0.3", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^8.0.1", - "tar": "^6.0.2", - "unique-filename": "^1.1.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/cacache/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "optional": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cacache/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cacache/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "optional": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cacache/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -7980,11 +7756,6 @@ "node": ">= 6" } }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, "node_modules/chrome-trace-event": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", @@ -8040,15 +7811,6 @@ "node": ">=0.10.0" } }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "optional": true, - "engines": { - "node": ">=6" - } - }, "node_modules/cli-tableau": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/cli-tableau/-/cli-tableau-2.0.1.tgz", @@ -9029,20 +8791,6 @@ "dev": true, "license": "MIT" }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -9082,14 +8830,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -9223,6 +8963,15 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -9600,6 +9349,7 @@ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "optional": true, + "peer": true, "dependencies": { "iconv-lite": "^0.6.2" } @@ -9629,14 +9379,6 @@ "node": ">=18" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -9673,21 +9415,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "optional": true - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -10590,7 +10317,6 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true, "license": "MIT" }, "node_modules/events": { @@ -10636,14 +10362,6 @@ "node": ">= 0.8.0" } }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "engines": { - "node": ">=6" - } - }, "node_modules/expect": { "version": "30.0.5", "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.5.tgz", @@ -10916,11 +10634,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" - }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -11069,7 +10782,6 @@ "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "dev": true, "funding": [ { "type": "individual", @@ -11311,11 +11023,6 @@ "node": ">= 0.8" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -11445,6 +11152,15 @@ "node": ">=10" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -11576,11 +11292,6 @@ "integrity": "sha512-2e/nZezdVlyCopOCYHeW0onkbZg7xP1Ad6pndPy1rCygeRykefUS6r7oA5cJRGEFvseiaz5a/qUHFVX1dd6Isg==", "dev": true }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -12071,12 +11782,6 @@ "entities": "^2.0.0" } }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "optional": true - }, "node_modules/http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", @@ -12118,7 +11823,6 @@ "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, "license": "MIT", "dependencies": { "eventemitter3": "^4.0.0", @@ -12133,7 +11837,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@tootallnate/once": "1", @@ -12145,28 +11849,20 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", - "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", - "dev": true, + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.5.tgz", + "integrity": "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==", "license": "MIT", "dependencies": { - "@types/http-proxy": "^1.17.8", + "@types/http-proxy": "^1.17.15", + "debug": "^4.3.6", "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" + "is-glob": "^4.0.3", + "is-plain-object": "^5.0.0", + "micromatch": "^4.0.8" }, "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/https-proxy-agent": { @@ -12192,15 +11888,6 @@ "node": ">=10.17.0" } }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "optional": true, - "dependencies": { - "ms": "^2.0.0" - } - }, "node_modules/hyperdyperid": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", @@ -12263,25 +11950,6 @@ "node": ">=4" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -12354,7 +12022,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -12369,12 +12037,6 @@ "node": ">=8" } }, - "node_modules/infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "optional": true - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -12396,6 +12058,7 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, "license": "ISC" }, "node_modules/internal-slot": { @@ -12416,7 +12079,7 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", - "devOptional": true, + "dev": true, "engines": { "node": ">= 12" } @@ -12624,7 +12287,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -12688,7 +12350,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -12732,12 +12393,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "optional": true - }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -12841,6 +12496,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -12853,6 +12517,12 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -13061,7 +12731,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -16527,6 +16197,12 @@ "node": ">=0.8.0" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -16559,6 +16235,21 @@ "yallist": "^3.0.2" } }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -16607,63 +16298,6 @@ "node": ">=10" } }, - "node_modules/make-fetch-happen": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", - "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", - "optional": true, - "dependencies": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/make-fetch-happen/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-fetch-happen/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/make-fetch-happen/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -16818,17 +16452,6 @@ "node": ">=6" } }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -16882,6 +16505,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -16897,161 +16521,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-collect/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, - "node_modules/minipass-fetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", - "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", - "optional": true, - "dependencies": { - "minipass": "^3.1.0", - "minipass-sized": "^1.0.3", - "minizlib": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "optionalDependencies": { - "encoding": "^0.1.12" - } - }, - "node_modules/minipass-fetch/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-fetch/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-flush/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, "node_modules/minizlib": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", @@ -17093,11 +16562,6 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" - }, "node_modules/module-details-from-path": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", @@ -17130,6 +16594,42 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "node_modules/mysql2": { + "version": "3.14.4", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.4.tgz", + "integrity": "sha512-Cs/jx3WZPNrYHVz+Iunp9ziahaG5uFMvD2R8Zlmc194AqXNxt9HBNu7ZsPYrUtmJsF0egETCWIdMIYAwOGjL1w==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -17142,6 +16642,27 @@ "thenify-all": "^1.0.0" } }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/nan": { "version": "2.23.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", @@ -17167,11 +16688,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -17228,7 +16744,7 @@ "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -17261,28 +16777,6 @@ "tslib": "^2.0.3" } }, - "node_modules/node-abi": { - "version": "3.75.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", - "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-addon-api": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", @@ -17343,92 +16837,6 @@ "node": ">= 6.13.0" } }, - "node_modules/node-gyp": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", - "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", - "optional": true, - "dependencies": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^9.1.0", - "nopt": "^5.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": ">= 10.12.0" - } - }, - "node_modules/node-gyp/node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "deprecated": "This package is no longer supported.", - "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/node-gyp/node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "deprecated": "This package is no longer supported.", - "optional": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/node-gyp/node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "deprecated": "This package is no longer supported.", - "optional": true, - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/node-gyp/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -17871,21 +17279,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "optional": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-retry": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", @@ -20081,31 +19474,6 @@ "dev": true, "license": "MIT" }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -20425,34 +19793,6 @@ "asap": "~2.0.6" } }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "optional": true - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "optional": true, - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/promise-retry/node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "optional": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/promptly": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/promptly/-/promptly-2.2.0.tgz", @@ -20613,15 +19953,6 @@ "url": "https://github.com/sponsors/lupomontero" } }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -20729,28 +20060,6 @@ "node": ">= 0.8" } }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -21180,6 +20489,31 @@ "node": ">= 0.6" } }, + "node_modules/react-scripts/node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, "node_modules/react-scripts/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -21769,7 +21103,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true, "license": "MIT" }, "node_modules/resolve": { @@ -22347,6 +21680,11 @@ "node": ">= 0.6" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -22638,6 +21976,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "devOptional": true, "funding": [ { "type": "github", @@ -22653,30 +21992,6 @@ } ] }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -22697,7 +22012,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "devOptional": true, + "dev": true, "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" @@ -22719,7 +22034,7 @@ "version": "2.8.7", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", - "devOptional": true, + "dev": true, "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" @@ -22729,20 +22044,6 @@ "npm": ">= 3.0.0" } }, - "node_modules/socks-proxy-agent": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", - "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", - "optional": true, - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -22860,64 +22161,15 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/sqlite3": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", - "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", - "hasInstallScript": true, - "dependencies": { - "bindings": "^1.5.0", - "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.1", - "tar": "^6.1.11" - }, - "optionalDependencies": { - "node-gyp": "8.x" - }, - "peerDependencies": { - "node-gyp": "8.x" - }, - "peerDependenciesMeta": { - "node-gyp": { - "optional": true - } - } - }, - "node_modules/sqlite3/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" - }, - "node_modules/ssri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", - "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", - "optional": true, - "dependencies": { - "minipass": "^3.1.1" - }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", "engines": { - "node": ">= 8" + "node": ">= 0.6" } }, - "node_modules/ssri/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ssri/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, "node_modules/stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", @@ -23808,32 +23060,6 @@ "node": ">=10" } }, - "node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/tar/node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -24264,17 +23490,6 @@ "dev": true, "license": "0BSD" }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/tv4": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz", @@ -24549,24 +23764,6 @@ "node": ">=4" } }, - "node_modules/unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "optional": true, - "dependencies": { - "unique-slug": "^2.0.0" - } - }, - "node_modules/unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "optional": true, - "dependencies": { - "imurmurhash": "^0.1.4" - } - }, "node_modules/unique-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", @@ -25188,6 +24385,31 @@ "node": ">= 0.6" } }, + "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, "node_modules/webpack-dev-server/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -25521,7 +24743,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" diff --git a/package.json b/package.json index b275613..918dcf7 100755 --- a/package.json +++ b/package.json @@ -2,19 +2,20 @@ "name": "deathwatch-roller", "version": "0.1.0", "private": true, + "proxy": "http://localhost:5000", "dependencies": { "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "bcrypt": "^5.1.0", - "better-sqlite3": "^12.2.0", "cors": "^2.8.5", "express": "^5.1.0", + "http-proxy-middleware": "^3.0.5", + "mysql2": "^3.14.4", "pdf-parse": "^1.1.1", "pdfjs-dist": "^3.9.179", "react": "^18.2.0", "react-dom": "^18.2.0", - "sqlite3": "^5.1.7", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/scripts/fill-talents.js b/scripts/fill-talents.js deleted file mode 100644 index 0e74eca..0000000 --- a/scripts/fill-talents.js +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env node -const https = require('https'); -const fs = require('fs'); -const path = require('path'); -const { db } = require('../database/sqlite-db'); - -function fetchUrl(url) { - return new Promise((resolve, reject) => { - https.get(url, { headers: { 'User-Agent': 'dwroller-bot/1.0' } }, (res) => { - let data = ''; - res.on('data', (c) => data += c); - res.on('end', () => resolve({ status: res.statusCode, body: data })); - }).on('error', reject); - }); -} - -function sanitizeText(s) { - if (!s) return ''; - let t = String(s); - t = t.replace(/Explore More/ig, ''); - t = t.replace(/Skip to content/ig, ''); - t = t.replace(/40k-?RPG-?FFG Wiki/ig, ''); - t = t.replace(/Explore Main Page/ig, ''); - t = t.replace(/^(Category:|Special:|Local sitemap).*/gi, ''); - t = t.replace(/\[\d+\]/g, ''); - t = t.replace(/\s+/g, ' ').trim(); - t = t.replace(/^This (article|page) .*/i, ''); - return t.trim(); -} - -function extractContentFromFandom(html) { - const out = { paragraphs: [], sourceLines: [] }; - const m = html.match(/