const express = require('express'); const sqlite3 = require('sqlite3').verbose(); const path = require('path'); const bcrypt = require('bcryptjs'); const crypto = require('crypto'); const fs = require('fs'); const os = require('os'); const multer = require('multer'); const app = express(); // Configure multer for file uploads const storage = multer.diskStorage({ destination: function (req, file, cb) { const uploadDir = './uploads/games'; if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } cb(null, uploadDir); }, filename: function (req, file, cb) { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); cb(null, 'game-' + uniqueSuffix + path.extname(file.originalname)); } }); const upload = multer({ storage: storage, limits: { fileSize: 50 * 1024 * 1024 // 50MB max }, fileFilter: function (req, file, cb) { const allowedExtensions = ['.rbxl', '.rbxlx']; const ext = path.extname(file.originalname).toLowerCase(); if (allowedExtensions.includes(ext)) { cb(null, true); } else { cb(new Error('Only .rbxl and .rbxlx files are allowed')); } } }); // Middleware app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(express.static(path.join(__dirname))); // Routes app.get('/', (req, res) => res.sendFile(path.join(__dirname, 'register.html'))); // Database setup const db = new sqlite3.Database('users.db', (err) => { if (err) console.error('DB connection failed:', err.message); else console.log('Connected to SQLite database.'); }); // Create tables db.serialize(() => { db.run(`CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, email TEXT UNIQUE, password TEXT, bio TEXT DEFAULT '', tix INTEGER DEFAULT 100, reset_token TEXT, reset_expires INTEGER, banned_until INTEGER, warned_until INTEGER, created_at INTEGER DEFAULT (strftime('%s', 'now')) )`); db.run(`CREATE TABLE IF NOT EXISTS games ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT, creator_id INTEGER NOT NULL, creator_name TEXT NOT NULL, thumbnail_url TEXT, game_file_url TEXT, active_players INTEGER DEFAULT 0, total_visits INTEGER DEFAULT 0, likes INTEGER DEFAULT 0, dislikes INTEGER DEFAULT 0, max_players INTEGER DEFAULT 10, genre TEXT, is_published BOOLEAN DEFAULT 1, is_approved BOOLEAN DEFAULT 0, created_at INTEGER DEFAULT (strftime('%s', 'now')), updated_at INTEGER DEFAULT (strftime('%s', 'now')), FOREIGN KEY(creator_id) REFERENCES users(id) )`); db.run(`CREATE TABLE IF NOT EXISTS groups ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT, owner_id INTEGER NOT NULL, owner_name TEXT NOT NULL, thumbnail_url TEXT, member_count INTEGER DEFAULT 1, is_approved BOOLEAN DEFAULT 0, created_at INTEGER DEFAULT (strftime('%s', 'now')), FOREIGN KEY(owner_id) REFERENCES users(id) )`); db.run(`CREATE TABLE IF NOT EXISTS group_members ( id INTEGER PRIMARY KEY AUTOINCREMENT, group_id INTEGER NOT NULL, user_id INTEGER NOT NULL, username TEXT NOT NULL, role TEXT DEFAULT 'Member', joined_at INTEGER DEFAULT (strftime('%s', 'now')), FOREIGN KEY(group_id) REFERENCES groups(id), FOREIGN KEY(user_id) REFERENCES users(id) )`); db.run(`CREATE TABLE IF NOT EXISTS pending_approvals ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, item_id INTEGER NOT NULL, item_name TEXT NOT NULL, creator_id INTEGER, creator_name TEXT, status TEXT DEFAULT 'pending', created_at INTEGER DEFAULT (strftime('%s', 'now')), reviewed_at INTEGER, reviewed_by TEXT )`); db.run(`CREATE TABLE IF NOT EXISTS game_servers ( id INTEGER PRIMARY KEY AUTOINCREMENT, game_id INTEGER NOT NULL, server_ip TEXT, server_port INTEGER, current_players INTEGER DEFAULT 0, max_players INTEGER DEFAULT 10, is_active BOOLEAN DEFAULT 1, created_at INTEGER DEFAULT (strftime('%s', 'now')), FOREIGN KEY(game_id) REFERENCES games(id) )`); db.run(`CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)`); console.log('All tables ready.'); }); const ADMIN_EMAIL = 'hasanbeg4u@gmail.com'; function isAdminEmail(email, callback) { if (!email) return callback(false); const emailStr = String(email).toLowerCase().trim(); if (emailStr === ADMIN_EMAIL.toLowerCase()) return callback(true); db.get('SELECT username FROM users WHERE email = ? LIMIT 1', [email], (err, row) => { if (err || !row) return callback(false); if (String(row.username || '').toLowerCase() === 'nuki') return callback(true); return callback(false); }); } function parseIds(arr) { if (!Array.isArray(arr)) return []; return arr.map(x => Number(x)).filter(n => Number.isInteger(n) && n > 0); } // ============ USER ROUTES ============ app.post('/register', (req, res) => { const { username, email, password } = req.body; if (!username || !email || !password) return res.status(400).json({ ok: false, message: 'All fields required' }); const hashed = bcrypt.hashSync(password, 10); db.run('INSERT INTO users (username, email, password, tix, bio) VALUES (?, ?, ?, ?, ?)', [username, email, hashed, 100, ''], function (err) { if (err) return res.status(400).json({ ok: false, message: err.message }); res.json({ ok: true, message: 'Registered', data: { id: this.lastID, username, email, tix: 100, bio: '', created_at: Math.floor(Date.now() / 1000) } }); }); }); app.post('/login', (req, res) => { const { username, password } = req.body; if (!username || !password) return res.status(400).json({ ok: false, message: 'Username and password required' }); db.get('SELECT id, username, email, password, COALESCE(tix,0) as tix, bio, created_at FROM users WHERE (username = ? OR email = ?) LIMIT 1', [username, username], (err, row) => { if (err) return res.status(500).json({ ok: false, message: 'DB error' }); if (!row) return res.status(401).json({ ok: false, message: 'User not found' }); if (!bcrypt.compareSync(password, row.password)) return res.status(401).json({ ok: false, message: 'Invalid password' }); res.json({ ok: true, message: 'Logged in', data: { id: row.id, username: row.username, email: row.email, tix: row.tix, bio: row.bio || '', created_at: row.created_at }}); }); }); app.get('/user/:id', (req, res) => { const id = Number(req.params.id); if (!id) return res.status(400).json({ ok: false, message: 'Invalid ID' }); db.get('SELECT id, username, COALESCE(tix,0) as tix, bio, created_at FROM users WHERE id = ?', [id], (err, row) => { if (err) return res.status(500).json({ ok: false, message: 'DB error' }); if (!row) return res.status(404).json({ ok: false, message: 'Not found' }); res.json({ ok: true, data: row }); }); }); app.put('/user/profile', (req, res) => { const { user_id, bio } = req.body; if (!user_id) return res.status(400).json({ ok: false, message: 'User ID required' }); db.run('UPDATE users SET bio = ? WHERE id = ?', [bio || '', user_id], function(err) { if (err) return res.status(500).json({ ok: false, message: 'Update failed' }); res.json({ ok: true, message: 'Profile updated' }); }); }); app.post('/save-client-token', (req, res) => { const { userId, authToken } = req.body; if (!userId || !authToken) { return res.status(400).json({ ok: false, message: 'Missing data' }); } try { const platform = os.platform(); let tokenDir; if (platform === 'win32') { tokenDir = path.join(process.env.APPDATA || '', 'Bloxoria'); } else if (platform === 'darwin') { tokenDir = path.join(os.homedir(), 'Library', 'Application Support', 'Bloxoria'); } else { tokenDir = path.join(os.homedir(), '.config', 'Bloxoria'); } if (!fs.existsSync(tokenDir)) { fs.mkdirSync(tokenDir, { recursive: true }); } const tokenPath = path.join(tokenDir, 'auth.token'); fs.writeFileSync(tokenPath, authToken, 'utf8'); const userPath = path.join(tokenDir, 'user.json'); fs.writeFileSync(userPath, JSON.stringify({ userId, timestamp: Date.now() }), 'utf8'); console.log('Token saved to:', tokenPath); res.json({ ok: true, message: 'Token saved' }); } catch (error) { console.error('Error saving token:', error); res.json({ ok: true, message: 'Token save attempted' }); } }); app.post('/forgot', (req, res) => { const { email } = req.body; if (!email) return res.status(400).json({ ok: false, message: 'Email required' }); db.get('SELECT id FROM users WHERE lower(email)=lower(?)', [email], (err, row) => { if (err) return res.status(500).json({ ok: false, message: 'DB error' }); if (!row) return res.json({ ok: true, message: 'If account exists, email sent' }); const token = crypto.randomBytes(32).toString('hex'); const expires = Date.now() + 3600000; db.run('UPDATE users SET reset_token = ?, reset_expires = ? WHERE id = ?', [token, expires, row.id], () => { const resetLink = `${req.protocol}://${req.get('host')}/reset.html?token=${token}`; console.log('Reset link:', resetLink); res.json({ ok: true, message: 'If account exists, email sent' }); }); }); }); app.post('/reset', (req, res) => { const { token, password } = req.body; if (!token || !password) return res.status(400).json({ ok: false, message: 'Token and password required' }); db.get('SELECT id, reset_expires FROM users WHERE reset_token = ?', [token], (err, row) => { if (err) return res.status(500).json({ ok: false, message: 'DB error' }); if (!row || row.reset_expires < Date.now()) return res.status(400).json({ ok: false, message: 'Invalid/expired token' }); const hashed = bcrypt.hashSync(password, 10); db.run('UPDATE users SET password = ?, reset_token = NULL, reset_expires = NULL WHERE id = ?', [hashed, row.id], (err) => { if (err) return res.status(500).json({ ok: false, message: 'Update failed' }); res.json({ ok: true, message: 'Password updated' }); }); }); }); // ============ SEARCH ROUTES ============ app.get('/search/users', (req, res) => { const query = req.query.q || ''; if (!query) return res.json({ ok: true, data: [] }); db.all('SELECT id, username FROM users WHERE username LIKE ? LIMIT 10', [`%${query}%`], (err, rows) => { if (err) return res.status(500).json({ ok: false, message: 'DB error' }); res.json({ ok: true, data: rows || [] }); }); }); app.get('/search/groups', (req, res) => { const query = req.query.q || ''; if (!query) return res.json({ ok: true, data: [] }); db.all('SELECT id, name, member_count FROM groups WHERE name LIKE ? AND is_approved = 1 LIMIT 10', [`%${query}%`], (err, rows) => { if (err) return res.status(500).json({ ok: false, message: 'DB error' }); res.json({ ok: true, data: rows || [] }); }); }); // ============ ADMIN ROUTES ============ app.get('/admin/users', (req, res) => { const adminEmail = req.query.adminEmail; isAdminEmail(adminEmail, (isAdmin) => { if (!isAdmin) return res.status(403).json({ ok: false, message: 'Forbidden' }); db.all('SELECT id, username, email, COALESCE(tix,0) as tix, banned_until FROM users ORDER BY id DESC', (err, rows) => { if (err) return res.status(500).json({ ok: false, message: 'DB error' }); res.json({ ok: true, data: rows }); }); }); }); app.get('/admin/pending', (req, res) => { const adminEmail = req.query.adminEmail; isAdminEmail(adminEmail, (isAdmin) => { if (!isAdmin) return res.status(403).json({ ok: false, message: 'Forbidden' }); db.all('SELECT * FROM pending_approvals WHERE status = ? ORDER BY created_at DESC', ['pending'], (err, rows) => { if (err) return res.status(500).json({ ok: false, message: 'DB error' }); res.json({ ok: true, data: rows || [] }); }); }); }); app.post('/admin/approve', (req, res) => { const { adminEmail, approval_id, approve } = req.body; isAdminEmail(adminEmail, (isAdmin) => { if (!isAdmin) return res.status(403).json({ ok: false, message: 'Forbidden' }); db.get('SELECT * FROM pending_approvals WHERE id = ?', [approval_id], (err, approval) => { if (err || !approval) return res.status(500).json({ ok: false, message: 'Approval not found' }); const status = approve ? 'approved' : 'rejected'; const now = Math.floor(Date.now() / 1000); db.run('UPDATE pending_approvals SET status = ?, reviewed_at = ?, reviewed_by = ? WHERE id = ?', [status, now, adminEmail, approval_id], (err) => { if (err) return res.status(500).json({ ok: false, message: 'Update failed' }); if (approve) { if (approval.type === 'game') { db.run('UPDATE games SET is_approved = 1 WHERE id = ?', [approval.item_id]); } else if (approval.type === 'group') { db.run('UPDATE groups SET is_approved = 1 WHERE id = ?', [approval.item_id]); } } res.json({ ok: true, message: status }); }); }); }); }); app.post('/admin/set_tix', (req, res) => { const { adminEmail, ids, amount } = req.body; isAdminEmail(adminEmail, (isAdmin) => { if (!isAdmin) return res.status(403).json({ ok: false, message: 'Forbidden' }); const idsList = parseIds(ids); if (idsList.length === 0) return res.status(400).json({ ok: false, message: 'No IDs' }); const amt = Number(amount); if (!Number.isFinite(amt)) return res.status(400).json({ ok: false, message: 'Invalid amount' }); const placeholders = idsList.map(() => '?').join(','); db.run(`UPDATE users SET tix = ? WHERE id IN (${placeholders})`, [amt, ...idsList], function (err) { if (err) return res.status(500).json({ ok: false, message: 'DB error' }); res.json({ ok: true, rowsChanged: this.changes }); }); }); }); app.post('/admin/ban', (req, res) => { const { adminEmail, ids, days } = req.body; isAdminEmail(adminEmail, (isAdmin) => { if (!isAdmin) return res.status(403).json({ ok: false, message: 'Forbidden' }); const idsList = parseIds(ids); if (idsList.length === 0) return res.status(400).json({ ok: false, message: 'No IDs' }); const until = Date.now() + (Number(days) || 1) * 86400000; const placeholders = idsList.map(() => '?').join(','); db.run(`UPDATE users SET banned_until = ? WHERE id IN (${placeholders})`, [until, ...idsList], function (err) { if (err) return res.status(500).json({ ok: false, message: 'DB error' }); res.json({ ok: true, rowsChanged: this.changes }); }); }); }); // ============ GAME ROUTES ============ app.get('/games', (req, res) => { const { search, sort, genre } = req.query; let sql = 'SELECT * FROM games WHERE is_published = 1 AND is_approved = 1'; const params = []; if (search) { sql += ' AND (title LIKE ? OR description LIKE ?)'; params.push(`%${search}%`, `%${search}%`); } if (genre) { sql += ' AND genre = ?'; params.push(genre); } if (sort === 'popular') sql += ' ORDER BY total_visits DESC'; else if (sort === 'recent') sql += ' ORDER BY created_at DESC'; else if (sort === 'liked') sql += ' ORDER BY likes DESC'; else sql += ' ORDER BY created_at DESC'; sql += ' LIMIT 50'; db.all(sql, params, (err, rows) => { if (err) return res.status(500).json({ ok: false, message: 'DB error' }); res.json({ ok: true, data: rows }); }); }); app.get('/games/:id', (req, res) => { const id = Number(req.params.id); if (!id) return res.status(400).json({ ok: false, message: 'Invalid ID' }); db.get('SELECT * FROM games WHERE id = ?', [id], (err, row) => { if (err) return res.status(500).json({ ok: false, message: 'DB error' }); if (!row) return res.status(404).json({ ok: false, message: 'Game not found' }); res.json({ ok: true, data: row }); }); }); app.post('/games', (req, res) => { const { title, description, creator_id, creator_name, thumbnail_url, game_file_url, max_players, genre } = req.body; if (!title || !creator_id || !creator_name) { return res.status(400).json({ ok: false, message: 'Title, creator_id, and creator_name required' }); } const sql = `INSERT INTO games (title, description, creator_id, creator_name, thumbnail_url, game_file_url, max_players, genre, is_approved) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)`; db.run(sql, [title, description || '', creator_id, creator_name, thumbnail_url || '', game_file_url || '', max_players || 10, genre || 'All'], function (err) { if (err) return res.status(500).json({ ok: false, message: 'Failed to create game' }); const gameId = this.lastID; db.run('INSERT INTO pending_approvals (type, item_id, item_name, creator_id, creator_name) VALUES (?, ?, ?, ?, ?)', ['game', gameId, title, creator_id, creator_name]); res.json({ ok: true, message: 'Game published (pending approval)', data: { id: gameId } }); }); }); app.post('/games/:id/visit', (req, res) => { const id = Number(req.params.id); if (!id) return res.status(400).json({ ok: false, message: 'Invalid ID' }); db.run('UPDATE games SET total_visits = total_visits + 1 WHERE id = ?', [id], function (err) { if (err) return res.status(500).json({ ok: false, message: 'Update failed' }); res.json({ ok: true }); }); }); // ============ FILE UPLOAD ROUTE ============ app.post('/upload-game', upload.single('game_file'), (req, res) => { if (!req.file) { return res.status(400).json({ ok: false, message: 'No file uploaded' }); } const { title, description, genre, max_players, creator_id, creator_name } = req.body; if (!title || !creator_id || !creator_name) { // Delete uploaded file if validation fails fs.unlinkSync(req.file.path); return res.status(400).json({ ok: false, message: 'Missing required fields' }); } // Store file path in database const gameFileUrl = '/uploads/games/' + req.file.filename; const sql = `INSERT INTO games ( title, description, creator_id, creator_name, game_file_url, max_players, genre, is_approved ) VALUES (?, ?, ?, ?, ?, ?, ?, 0)`; db.run(sql, [ title, description || '', creator_id, creator_name, gameFileUrl, max_players || 10, genre || 'All' ], function (err) { if (err) { console.error('Database error:', err); fs.unlinkSync(req.file.path); return res.status(500).json({ ok: false, message: 'Failed to save game' }); } const gameId = this.lastID; // Add to pending approvals db.run( 'INSERT INTO pending_approvals (type, item_id, item_name, creator_id, creator_name) VALUES (?, ?, ?, ?, ?)', ['game', gameId, title, creator_id, creator_name] ); console.log(`Game uploaded: ${title} (ID: ${gameId}) by ${creator_name}`); res.json({ ok: true, message: 'Game uploaded successfully', data: { id: gameId, title: title, file: gameFileUrl } }); }); }); // Serve uploaded files app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); // Serve game files to client app.get('/asset', (req, res) => { const { id } = req.query; if (!id) { return res.status(400).send('Missing asset ID'); } db.get('SELECT game_file_url FROM games WHERE id = ?', [id], (err, game) => { if (err || !game || !game.game_file_url) { // Return default empty place if no file const defaultPlace = ` null nil Workspace `; res.setHeader('Content-Type', 'application/xml'); return res.send(defaultPlace); } const filePath = path.join(__dirname, game.game_file_url); if (!fs.existsSync(filePath)) { return res.status(404).send('Game file not found'); } res.setHeader('Content-Type', 'application/xml'); res.sendFile(filePath); }); }); // ============ GROUP ROUTES ============ app.get('/groups', (req, res) => { const { search, sort } = req.query; let sql = 'SELECT * FROM groups WHERE is_approved = 1'; const params = []; if (search) { sql += ' AND name LIKE ?'; params.push(`%${search}%`); } if (sort === 'popular') sql += ' ORDER BY member_count DESC'; else sql += ' ORDER BY created_at DESC'; sql += ' LIMIT 50'; db.all(sql, params, (err, rows) => { if (err) return res.status(500).json({ ok: false, message: 'DB error' }); res.json({ ok: true, data: rows || [] }); }); }); app.get('/groups/:id', (req, res) => { const id = Number(req.params.id); if (!id) return res.status(400).json({ ok: false, message: 'Invalid ID' }); db.get('SELECT * FROM groups WHERE id = ?', [id], (err, row) => { if (err) return res.status(500).json({ ok: false, message: 'DB error' }); if (!row) return res.status(404).json({ ok: false, message: 'Group not found' }); res.json({ ok: true, data: row }); }); }); app.post('/groups', (req, res) => { const { name, description, owner_id, owner_name, thumbnail_url } = req.body; if (!name || !owner_id || !owner_name) { return res.status(400).json({ ok: false, message: 'Name, owner_id, and owner_name required' }); } db.get('SELECT tix FROM users WHERE id = ?', [owner_id], (err, user) => { if (err || !user) return res.status(500).json({ ok: false, message: 'User not found' }); if (user.tix < 300) return res.status(400).json({ ok: false, message: 'Need 300 Tix to create group' }); db.run('UPDATE users SET tix = tix - 300 WHERE id = ?', [owner_id], (err) => { if (err) return res.status(500).json({ ok: false, message: 'Failed to deduct Tix' }); const sql = `INSERT INTO groups (name, description, owner_id, owner_name, thumbnail_url, is_approved) VALUES (?, ?, ?, ?, ?, 0)`; db.run(sql, [name, description || '', owner_id, owner_name, thumbnail_url || ''], function (err) { if (err) return res.status(500).json({ ok: false, message: 'Failed to create group' }); const groupId = this.lastID; db.run('INSERT INTO group_members (group_id, user_id, username, role) VALUES (?, ?, ?, ?)', [groupId, owner_id, owner_name, 'Owner']); db.run('INSERT INTO pending_approvals (type, item_id, item_name, creator_id, creator_name) VALUES (?, ?, ?, ?, ?)', ['group', groupId, name, owner_id, owner_name]); res.json({ ok: true, message: 'Group created (pending approval)', data: { id: groupId } }); }); }); }); }); // ============ DAILY TIX SYSTEM ============ setInterval(() => { const now = new Date(); const hour = now.getUTCHours(); if (hour === 11) { const today = now.toISOString().split('T')[0]; db.get('SELECT value FROM meta WHERE key = ?', ['last_daily_tix'], (err, row) => { if (err || (row && row.value === today)) return; db.run('UPDATE users SET tix = COALESCE(tix,0) + 15', function (e) { if (e) return console.error('Daily credit error:', e.message); console.log('Daily tix credited'); db.run('INSERT OR REPLACE INTO meta(key,value) VALUES(?,?)', ['last_daily_tix', today]); }); }); } }, 60000); // Start server const port = process.env.PORT || 3000; app.listen(port, () => console.log(`Bloxoria server running on port ${port}`));