/** * database.js * JSON file-based database module (replaces database.php) * Uses atomic writes and file locking for concurrency safety. */ const fs = require("fs"); const path = require("path"); const crypto = require("crypto"); // Git sync for persistent storage on GitHub let gitSync = null; try { gitSync = require("./git-sync"); } catch (e) { // git-sync module not available, no-op } class Database { constructor(dataDir) { this.dataDir = dataDir; this.usersFile = path.join(dataDir, "users.json"); this.keysFile = path.join(dataDir, "license_keys.json"); this.hitsFile = path.join(dataDir, "hits.json"); this.settingsFile = path.join(dataDir, "settings.json"); this.binLibraryFile = path.join(dataDir, "bin_library.json"); this.hitCountsFile = path.join(dataDir, "hit_counts.json"); // Ensure data directory exists if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }); } } // ─── Atomic file read/write ─────────────────────────────────── _readJson(filePath) { try { if (!fs.existsSync(filePath)) return []; const raw = fs.readFileSync(filePath, "utf-8"); const parsed = JSON.parse(raw); return parsed; } catch (e) { return []; } } _writeJson(filePath, data) { const tmp = filePath + ".tmp." + process.pid; try { fs.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8"); fs.renameSync(tmp, filePath); // Schedule git sync after successful write if (gitSync && gitSync.scheduleSync) gitSync.scheduleSync(); return true; } catch (e) { try { fs.unlinkSync(tmp); } catch (_) {} return false; } } _readObj(filePath) { try { if (!fs.existsSync(filePath)) return {}; const raw = fs.readFileSync(filePath, "utf-8"); return JSON.parse(raw) || {}; } catch (e) { return {}; } } // ─── Users ───────────────────────────────────────────────────── getAllUsers() { return this._readJson(this.usersFile); } getUser(userId) { const users = this.getAllUsers(); return users.find((u) => String(u.user_id) === String(userId)) || null; } getUserByToken(token) { const users = this.getAllUsers(); return users.find((u) => u.token === token) || null; } isTokenExists(token) { return !!this.getUserByToken(token); } createUser(data) { const users = this.getAllUsers(); const user = { user_id: data.user_id, username: data.username || "", first_name: data.first_name || "", pfp_url: data.pfp_url || "", token: data.token || null, hits: 0, attempts: 0, banned: false, created_at: new Date().toISOString(), last_active: new Date().toISOString(), }; users.push(user); this._writeJson(this.usersFile, users); return user; } updateUser(userId, updates) { const users = this.getAllUsers(); const idx = users.findIndex((u) => String(u.user_id) === String(userId)); if (idx === -1) return false; Object.assign(users[idx], updates); this._writeJson(this.usersFile, users); return true; } // ─── Hits ────────────────────────────────────────────────────── getAllHits() { return this._readJson(this.hitsFile); } addHitRecord(userId, details) { const hits = this.getAllHits(); hits.push({ user_id: String(userId), ...details, timestamp: new Date().toISOString(), }); this._writeJson(this.hitsFile, hits); } getHitCounts(userId) { const counts = this._readObj(this.hitCountsFile); return { global_hits: counts.global || 0, user_hits: userId ? counts.users?.[String(userId)] || 0 : 0, }; } incrementStat(userId, field) { const users = this.getAllUsers(); const idx = users.findIndex((u) => String(u.user_id) === String(userId)); if (idx === -1) return 0; users[idx][field] = (users[idx][field] || 0) + 1; this._writeJson(this.usersFile, users); // Also update hit_counts.json for fast O(1) reads if (field === "hits") { const counts = this._readObj(this.hitCountsFile); if (!counts.users) counts.users = {}; counts.users[String(userId)] = users[idx].hits; counts.global = (counts.global || 0) + 1; this._writeJson(this.hitCountsFile, counts); } return users[idx][field]; } getUserHits(userId, limit = 50) { const hits = this.getAllHits(); return hits .filter((h) => String(h.user_id) === String(userId)) .slice(-limit) .reverse(); } getUserHitsTotal(userId) { const hits = this.getAllHits(); return hits.filter((h) => String(h.user_id) === String(userId)).length; } // ─── License Keys ────────────────────────────────────────────── getAllLicenseKeys() { return this._readJson(this.keysFile); } createLicenseKey(key, version, active = true) { const keys = this.getAllLicenseKeys(); keys.push({ key, version, active, created_at: new Date().toISOString(), }); this._writeJson(this.keysFile, keys); } validateLicenseKey(key) { const keys = this.getAllLicenseKeys(); const found = keys.find((k) => k.key === key); if (!found) return { valid: false }; return { valid: found.active !== false, version: found.version }; } revokeLicenseKey(key) { const keys = this.getAllLicenseKeys(); const idx = keys.findIndex((k) => k.key === key); if (idx === -1) return false; keys[idx].active = false; this._writeJson(this.keysFile, keys); return true; } activateLicenseKey(key) { const keys = this.getAllLicenseKeys(); const idx = keys.findIndex((k) => k.key === key); if (idx === -1) return false; keys[idx].active = true; this._writeJson(this.keysFile, keys); return true; } deleteLicenseKey(key) { const keys = this.getAllLicenseKeys(); const idx = keys.findIndex((k) => k.key === key); if (idx === -1) return false; keys.splice(idx, 1); this._writeJson(this.keysFile, keys); return true; } // ─── Settings ────────────────────────────────────────────────── getSetting(name) { const settings = this._readObj(this.settingsFile); return settings[name] || null; } setSetting(name, value) { const settings = this._readObj(this.settingsFile); settings[name] = value; this._writeJson(this.settingsFile, settings); } getAllSettings() { return this._readObj(this.settingsFile); } // ─── BIN Library ─────────────────────────────────────────────── getBinLibrary() { return this._readJson(this.binLibraryFile); } saveBinLibrary(bins) { this._writeJson(this.binLibraryFile, bins); } addBinToLibrary(binObj) { const bins = this.getBinLibrary(); bins.push(binObj); return this._writeJson(this.binLibraryFile, bins); } removeBinFromLibrary(binId, binNumber) { const bins = this.getBinLibrary(); const idx = bins.findIndex( (b) => (binId && b.id === binId) || (binNumber && b.bin === binNumber), ); if (idx === -1) return null; const removed = bins.splice(idx, 1)[0]; this._writeJson(this.binLibraryFile, bins); return removed; } clearBinLibrary() { return this._writeJson(this.binLibraryFile, []); } // ─── Stats / Leaderboard ─────────────────────────────────────── getGlobalStats() { const users = this.getAllUsers(); const hits = this.getAllHits(); let totalAttempts = 0; let totalHits = 0; for (const u of users) { totalAttempts += u.attempts || 0; totalHits += u.hits || 0; } return { total_users: users.length, total_attempts: totalAttempts, total_hits: totalHits, }; } getTopUsers(limit = 10) { const users = this.getAllUsers(); return users .filter((u) => (u.hits || 0) > 0) .sort((a, b) => (b.hits || 0) - (a.hits || 0)) .slice(0, limit); } getUserRank(userId) { const users = this.getAllUsers(); const sorted = users .filter((u) => (u.hits || 0) > 0) .sort((a, b) => (b.hits || 0) - (a.hits || 0)); const idx = sorted.findIndex((u) => String(u.user_id) === String(userId)); if (idx === -1) return null; return { rank: idx + 1, user_id: String(sorted[idx].user_id), username: sorted[idx].username || "", hits: sorted[idx].hits || 0, }; } // ─── Admin ───────────────────────────────────────────────────── clearAllTokensAndStats() { const users = this.getAllUsers(); let count = 0; for (const u of users) { u.token = null; u.hits = 0; u.attempts = 0; count++; } this._writeJson(this.usersFile, users); this._writeJson(this.hitCountsFile, { global: 0, users: {} }); return count; } cleanFakeHits() { const hits = this.getAllHits(); const before = hits.length; const cleaned = hits.filter( (h) => h.full_card && h.full_card.length >= 13 && h.full_card.includes("|"), ); this._writeJson(this.hitsFile, cleaned); return before - cleaned.length; } resetDatabase() { const files = [this.usersFile, this.keysFile, this.hitsFile, this.hitCountsFile]; let deleted = 0; for (const f of files) { if (fs.existsSync(f)) { fs.unlinkSync(f); deleted++; } } return deleted; } rebuildHitCounters() { const users = this.getAllUsers(); const counts = { global: 0, users: {} }; let usersWithHits = 0; for (const u of users) { const h = u.hits || 0; if (h > 0) { counts.users[String(u.user_id)] = h; counts.global += h; usersWithHits++; } } this._writeJson(this.hitCountsFile, counts); return { total_hits: counts.global, users_with_hits: usersWithHits }; } migrateOldHitCounters() { // No-op for JS version (migration was for old PHP data) } autoSyncHitCounters() { if (!fs.existsSync(this.hitCountsFile)) { this.rebuildHitCounters(); } } } module.exports = Database;