282 lines
10 KiB
JavaScript
282 lines
10 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const mariadb = require('mariadb');
|
|
const reasonFlags = JSON.parse(process.env.REASON_FLAGS)
|
|
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 = mariadb.createPool({
|
|
host: process.env.DB_HOST, // Replace with your database host
|
|
port: process.env.DB_PORT || 3306,
|
|
user: process.env.DB_USER, // Replace with your database username
|
|
password: process.env.DB_PASS, // Replace with your database password
|
|
// Replace with your database name
|
|
connectionLimit: 1000 // 0 means no limit
|
|
});
|
|
|
|
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];
|
|
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 });
|
|
});
|
|
|
|
router.post('/login', 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; |