Spaces:
Sleeping
Sleeping
| /** | |
| * 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; | |