const express = require('express'); const router = express.Router(); const mariadb = require('mariadb'); const reasonFlags = global.reasonFlags const expressSession = require('express-session'); const bcrypt = require("bcrypt") const crypto = require("crypto") const flags = require('../flags'); const { execSync } = require('child_process'); const { env } = require('process'); const session = require('express-session'); const totp = require('totp-generator').TOTP; const multer = require('multer'); const csv = require('csv-parser'); const fs = require('fs'); // Create a MariaDB connection pool const pool = global.db_pool router.use(express.json()); router.use(express.urlencoded({ extended: true })); router.use(expressSession({ store: expressSession.MemoryStore(), secret: process.env.SESSION_SECRET || 'default_secret', resave: false, saveUninitialized: false, cookie: { secure: process.env.NODE_ENV === 'production', maxAge: 24 * 60 * 60 * 1000 // 24 hours } })); const authenticate = (req, res, next) => { if (!req.session.admin) { res.redirect('/admin/login'); return; } next(); } const auditLog = async (action, data, user) => { const conn = await pool.getConnection(); await conn.query('INSERT INTO audit_logs (action, data, user) VALUES (?, ?, ?)', [action, data, user]); conn.end(); } // MAIN PAGES router.get('/', authenticate, (req, res) => { res.render('admin/dashboard', { env: process.env, session: req.session }); }); router.get('/edit/:id', authenticate, async (req, res) => { const conn = await pool.getConnection(); const id = req.params.id; const row = await conn.query('SELECT * FROM bans WHERE id = ?', [id]); conn.end(); if (!row[0]) { res.redirect('/admin'); return; } res.render('admin/edit', { env: process.env, session: req.session, ban: row[0], reasonFlags, setFlags: flags.getSetFlags(row[0].reasonsFlag, reasonFlags) }); }); // Ban Creation router.get('/create', authenticate, (req, res) => { res.render('admin/create', { env: process.env, session: req.session, reasonFlags }); }); router.post('/create', authenticate, async (req, res) => { const conn = await pool.getConnection(); const data = req.body; if (!data.robloxId && !data.discordId) { res.json({ success: false, message: 'Please enter a Roblox ID or Discord ID.' }); return; } const reasonShort = data.reasonShort || 'No reason provided'; const reasonLong = data.reasonLong || 'No reason provided'; const reasonsFlag = data.reasonsFlag || 0; const moderator = req.session.user.username || 'Unknown'; const expiresTimestamp = data.expiresTimestamp || null; const robloxId = data.robloxId || null; const discordId = data.discordId || null; const discordUsername = data.discordUsername || null; const robloxUsername = data.robloxUsername || null; await conn.query('INSERT INTO bans (reasonShort, reasonLong, reasonsFlag, moderator, expiresTimestamp, robloxId, discordId, robloxUsername, discordUsername) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', [reasonShort, reasonLong, reasonsFlag, moderator, expiresTimestamp, robloxId, discordId, robloxUsername, discordUsername]); conn.end(); auditLog('ban_create', { robloxId, discordId, moderator, reasonShort, reasonLong, reasonsFlag, expiresTimestamp }, req.session.user.username); res.json({ success: true, message: 'User banned successfully', redirect: '/admin' }); }); // Ban Editing router.post('/edit/:id', authenticate, async (req, res) => { const conn = await pool.getConnection(); const id = req.params.id; const data = req.body; const originalData = await conn.query('SELECT * FROM bans WHERE id = ?', [id]); if (!data.robloxId && !data.discordId) { res.json({ success: false, message: 'Please enter a Roblox ID or Discord ID.' }); return; } const reasonShort = data.reasonShort || 'No reason provided'; const reasonLong = data.reasonLong || 'No reason provided'; const reasonsFlag = data.reasonsFlag || 0; const expiresTimestamp = data.expiresTimestamp || null; const robloxId = data.robloxId || null; const discordId = data.discordId || null; const discordUsername = data.discordUsername || null; const robloxUsername = data.robloxUsername || null; await conn.query('UPDATE bans SET reasonShort = ?, reasonLong = ?, reasonsFlag = ?, expiresTimestamp = ?, robloxId = ?, discordId = ?, robloxUsername = ?, discordUsername = ? WHERE id = ?', [reasonShort, reasonLong, reasonsFlag, expiresTimestamp, robloxId, discordId, robloxUsername, discordUsername, id]); conn.end(); auditLog('ban_edit', { old: originalData, new: { robloxId, discordId, reasonShort, reasonLong, reasonsFlag, expiresTimestamp } }, req.session.user.username); res.json({ success: true, message: 'User updated successfully', redirect: '/admin' }); }); router.get("/import", authenticate, (req, res) => { res.render('admin/import', { env: process.env, session: req.session }); }); const upload = multer({ dest: 'uploads/' }); uploads = {}; // Storing upload progress router.post('/import', authenticate, upload.single('fileInput'), async (req, res) => { const fileType = req.body.fileType; const filePath = req.file.path; const uploadId = crypto.randomBytes(16).toString('hex'); if (fileType === 'csv') { const results = []; fs.createReadStream(filePath) .pipe(csv()) .on('data', (data) => results.push(data)) .on('end', async () => { const conn = await pool.getConnection(); uploads[uploadId] = { completed: false, total: results.length, processed: 0 }; res.end(JSON.stringify({ success: true, message: 'Started upload, please wait!', id: uploadId })); results.shift(); // Remove the first line if it is headers for (const row of results) { const { robloxId, discordId, robloxUsername, discordUsername, reasonShort, reasonLong } = row; await conn.query('INSERT INTO bans (robloxId, discordId, reasonShort, reasonLong, reasonsFlag, expiresTimestamp, robloxUsername, discordUsername) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [robloxId, discordId, reasonShort, reasonLong, robloxUsername, discordUsername]); uploads[uploadId].processed++; } conn.end(); fs.unlinkSync(filePath); // Remove the file after processing uploads[uploadId].completed = true; setTimeout(() => { delete uploads[uploadId]; }, 60000); // Remove the upload after 1 minute }); } else if (fileType === 'mfd') { const data = fs.readFileSync(filePath, 'utf-8'); const lines = data.split('\n'); const conn = await pool.getConnection(); uploads[uploadId] = { completed: false, total: lines.length, processed: 0, skipped: 0 }; res.end(JSON.stringify({ success: true, message: 'Started upload, please wait!', id: uploadId })); for (const line of lines) { // This is a text file, split by "x/"", get [0], then split by / and get the last element, that will be an ID. All other values are static. const robloxId = line.split('x/')[0].split('/').pop(); let reasonLong = line.split('/profile ')[1] || "Undefined"; if (reasonLong?.length > 255) { reasonLong = reasonLong.substring(0, 2048); } const reasonShort = "Listed by MFD"; const moderator = "MFD"; const reasonsFlag = flags.addFlag(0, reasonFlags.CHILD_SAFETY) const existingBan = await conn.query('SELECT * FROM bans WHERE robloxId = ?', [robloxId]); if (existingBan.length > 0) { uploads[uploadId].processed++; uploads[uploadId].skipped++ continue; // Skip this entry if robloxId already exists } await conn.query('INSERT INTO bans (robloxId, reasonShort, reasonLong, reasonsFlag, moderator) VALUES (?, ?, ?, ?, ?)', [robloxId, reasonShort, reasonLong, reasonsFlag, moderator]); uploads[uploadId].processed++; } conn.end(); uploads[uploadId].completed = true; fs.unlinkSync(filePath); // Remove the file after processing } else { fs.unlinkSync(filePath); // Remove the file if the type is not supported res.json({ success: false, message: 'Unsupported file type.' }); } setTimeout(() => { delete uploads[uploadId]; }, 60000); // Remove the upload after 1 minute }); router.get('/uploadStatus', authenticate, (req, res) => { res.render('admin/uploadStatus', { env: process.env, session: req.session, uploads }); }); // API STUFF // router.get("/api/bans", authenticate, async (req, res) => { const conn = await pool.getConnection(); const rows = await conn.query('SELECT * FROM bans'); conn.end(); res.json(rows); }); router.get('/api/uploads/:id', authenticate, (req, res) => { const id = req.params.id; if (!uploads[id]) { res.json({ success: false, message: 'Upload not found' }); return; } res.json(uploads[id]); }); router.get('/api/uploads', authenticate, (req, res) => { res.json(uploads); }); // AUTH STUFF // router.get('/login', (req, res) => { if (req.session.admin) { res.redirect('/admin'); return; } res.render('admin/login', { env: process.env }); }); const rateLimit = require('../rateLimit.js'); router.post('/login', rateLimit.middleware, async (req, res) => { const conn = await pool.getConnection(); const username = req.body.username; const password = req.body.password; const row = await conn.query('SELECT * FROM users WHERE username = ?', [username]); conn.end(); if (row[0]) { const user = row[0]; const match = await bcrypt.compare(password, user.passwordHash); if (match) { if (user.totp_token) { if (!req.body.totp) { return res.json({ success: false, totpRequired: true, message: 'Please enter your 2FA code!' }); } const generatedToken = totp.generate(user.totp_token).otp; if (req.body.totp !== generatedToken) { return res.json({ success: false, totpRequired: true, message: 'Invalid TOTP token' }); } } req.session.admin = true; req.session.user = user; delete req.session.user.passwordHash; // Security measure return res.json({ success: true, message: 'Login successful', redirect: '/admin' }); } } res.json({ success: false, message: 'Invalid username or password' }); }); router.all('/logout', (req, res) => { req.session.destroy(); res.redirect('/admin/login'); }); module.exports = router;