UBS/routes/admin.js
2024-12-22 17:50:19 -07:00

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;