Add import feature

This commit is contained in:
Christopher Cookman 2024-12-22 17:10:45 -07:00
parent dae17e0f7e
commit 9b34e3d339
31 changed files with 379073 additions and 5 deletions

2
.gitignore vendored
View file

@ -128,3 +128,5 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
uploads/

159
package-lock.json generated
View file

@ -11,12 +11,14 @@
"dependencies": {
"bcrypt": "^5.1.1",
"colors": "^1.4.0",
"csv-parser": "^3.0.0",
"discord.js": "^14.16.3",
"dotenv": "^16.4.7",
"ejs": "^3.1.10",
"express": "^4.21.1",
"express-session": "^1.18.1",
"mariadb": "^3.4.0",
"multer": "^1.4.5-lts.1",
"noblox.js": "^6.0.2",
"totp-generator": "^1.0.0",
"uuid": "^11.0.3"
@ -441,6 +443,12 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
"node_modules/aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
@ -616,6 +624,23 @@
"base64-js": "^1.1.2"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT"
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -767,6 +792,51 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"license": "MIT"
},
"node_modules/concat-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"engines": [
"node >= 0.8"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^2.2.2",
"typedarray": "^0.0.6"
}
},
"node_modules/concat-stream/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/concat-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/concat-stream/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
@ -843,6 +913,21 @@
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/csv-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.0.0.tgz",
"integrity": "sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.0"
},
"bin": {
"csv-parser": "bin/csv-parser"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@ -1741,6 +1826,12 @@
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
"license": "MIT"
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@ -1977,6 +2068,15 @@
"node": "*"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
@ -2029,6 +2129,36 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/multer": {
"version": "1.4.5-lts.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
"integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.0.0",
"concat-stream": "^1.5.2",
"mkdirp": "^0.5.4",
"object-assign": "^4.1.1",
"type-is": "^1.6.4",
"xtend": "^4.0.0"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/multer/node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@ -2312,6 +2442,12 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -2630,6 +2766,14 @@
"bluebird": "^2.6.2"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@ -2764,6 +2908,12 @@
"node": ">= 0.6"
}
},
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
@ -2964,6 +3114,15 @@
}
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",

View file

@ -12,12 +12,14 @@
"dependencies": {
"bcrypt": "^5.1.1",
"colors": "^1.4.0",
"csv-parser": "^3.0.0",
"discord.js": "^14.16.3",
"dotenv": "^16.4.7",
"ejs": "^3.1.10",
"express": "^4.21.1",
"express-session": "^1.18.1",
"mariadb": "^3.4.0",
"multer": "^1.4.5-lts.1",
"noblox.js": "^6.0.2",
"totp-generator": "^1.0.0",
"uuid": "^11.0.3"

View file

@ -0,0 +1,62 @@
var messageElement = null;
var pollInterval = null;
var uploadId = null;
const poll = async function () {
try {
const pollResponse = await fetch(`/admin/api/uploads/${uploadId}`);
const pollResult = await pollResponse.json();
if (!pollResult.completed) {
messageElement.className = 'alert alert-info';
const percentage = ((pollResult.processed / pollResult.total) * 100).toFixed(2);
messageElement.textContent = `${pollResult.processed}/${pollResult.total} Imported. ${percentage}% done. You can leave this page.`;
} else {
clearInterval(pollInterval);
messageElement.className = 'alert alert-success';
messageElement.textContent = 'Import completed successfully. Redirecting...';
setTimeout(() => {
window.location.href = '/admin';
}, 1000);
}
} catch (pollError) {
clearInterval(pollInterval);
messageElement.className = 'alert alert-danger';
messageElement.textContent = 'An error occurred while polling the upload status.';
}
};
document.getElementById('banForm').addEventListener('submit', async function (event) {
event.preventDefault();
if (pollInterval !== null) {
return; // Upload already in progress
}
const form = event.target;
const formData = new FormData(form);
messageElement = document.getElementById('message');
messageElement.className = 'alert alert-info';
messageElement.textContent = 'Uploading... Please Wait';
messageElement.style.display = 'block';
try {
const response = await fetch(event.target.action, {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
Array.from(form.elements).forEach(element => element.disabled = true);
uploadId = result.id;
if (uploadId) {
poll(); // Start polling
pollInterval = setInterval(poll, 250); // Poll every 5 seconds
}
} else {
messageElement.className = 'alert alert-danger';
messageElement.textContent = result.message || 'Failed to import file.';
}
} catch (error) {
messageElement.className = 'alert alert-danger';
messageElement.textContent = 'An error occurred while importing the file.';
}
});

View file

@ -0,0 +1,23 @@
// Once the doc loads, find #import-status div, and start polling /admin/api/uploads to get full list
/* example response
{"51e4dd908c4d6dcffad46bc4b939712d":{"completed":true,"total":1,"processed":0},"b35467b9295d51c38d6cc4681cd18d02":{"completed":false,"total":18933,"processed":3383}}
*/
const poll = async function () {
try {
const pollResponse = await fetch(`/admin/api/uploads`);
const pollResult = await pollResponse.json();
const importStatus = document.getElementById('import-status');
importStatus.innerHTML = '';
for (const [uploadId, uploadData] of Object.entries(pollResult)) {
const div = document.createElement('div');
div.textContent = `${uploadId}: ${uploadData.processed}/${uploadData.total} Imported. ${((uploadData.processed / uploadData.total) * 100).toFixed(2)}% done.`;
importStatus.appendChild(div);
}
} catch (pollError) {
console.error('An error occurred while polling the upload status.');
}
setTimeout(poll, 250);
};
document.addEventListener('DOMContentLoaded', poll);

View file

@ -11,6 +11,9 @@ 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({
@ -94,7 +97,7 @@ router.post('/create', authenticate, async (req, res) => {
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 (?, ?, ?, ?, ?, ?, ?, ?, ?)',
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);
@ -106,7 +109,7 @@ 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) {
@ -122,13 +125,76 @@ router.post('/edit/:id', authenticate, async (req, res) => {
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 = ?',
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);
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;
});
} 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 };
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();
const reasonLong = line.split('/profile ')[1];
const reasonShort = "Listed by MFD";
const moderator = "MFD";
const reasonsFlag = flags.addFlag(0, reasonFlags.CHILD_SAFETY)
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.' });
}
});
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) => {
@ -138,6 +204,19 @@ router.get("/api/bans", authenticate, async (req, res) => {
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) => {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,6 @@
"id","robloxId","discordId","robloxUsername","discordUsername","reasonShort","reasonLong","reasonsFlag","moderator","banTimestamp","expiresTimestamp"
"1b5f4d5c-bfc6-11ef-8db2-e0d55eac39c4","1234",,,,asdf,asdf,3,admin,2024-12-21 18:05:10.000,2024-12-21 11:05:00.000
"216d4c19-bf76-11ef-8db2-e0d55eac39c4","4321","43211234",,,aasdf,asdfasdfasdf,9,admin,2024-12-21 08:32:39.000,
"54fdbfd6-bfc5-11ef-8db2-e0d55eac39c4","121234","21312341234",,,"42424","32423424",31,admin,2024-12-21 17:59:37.000,
"83bd09b5-bf78-11ef-8db2-e0d55eac39c4","25226480",,,,Edited,Guh,31,admin,2024-12-21 08:49:43.000,
e3ae0728-bfc5-11ef-8db2-e0d55eac39c4,"12344321","4123",,,asdfadsf,asdfasdf,0,admin,2024-12-21 18:03:37.000,

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
robloxId,discordId,robloxUsername,discordUsername,reasonShort,reasonLong
123654,,test,,Test Reason,test2

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -5,6 +5,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/assets/css/bootstrap.min.css">
<link href="https://cdn.jsdelivr.net/gh/hung1001/font-awesome-pro@4cac1a6/css/all.css" rel="stylesheet" type="text/css" />
<title>UBS Admin Dashboard</title>
</head>
@ -18,6 +19,14 @@
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<div class="d-flex">
<a href="/admin/import" class="btn btn-secondary me-2">
<i class="fas fa-file-import"></i> Import Bans
</a>
<a href="/admin/create" class="btn btn-primary">
<i class="fas fa-plus-circle"></i> New Ban
</a>
</div>
<li class="nav-item">
<a class="nav-link" href="#">Welcome, <%= session.user.username %></a>
</li>
@ -30,7 +39,6 @@
</nav>
<div class="container mt-5">
<h2 class="mb-4 d-inline">Bans</h2>
<a href="/admin/create" class="btn btn-primary float-end">New Ban</a>
</div>
<table class="table table-dark table-striped">
<thead>

41
views/admin/import.ejs Normal file
View file

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/assets/css/bootstrap.min.css">
<title>UBS Admin - New ban</title>
</head>
<body class="bg-dark text-light">
<div class="container">
<div class="row justify-content-center mt-5">
<div class="col-md-6 col-lg-4">
<div class="card bg-dark text-light shadow">
<div class="card-body p-4">
<h2 class="text-center mb-4">Mass Ban (Import)</h2>
<div class="alert" id="message" style="display: none;"></div>
<div class="alert alert-warning">
The import system does <strong>NOT</strong> have deduplication. Please be sure to remove duplicate entries from your list <strong>BEFORE</strong> uploading!
</div>
<form id="banForm">
<div class="mb-3">
<label for="fileType" class="form-label">Select File Type</label>
<select class="form-select" id="fileType" name="fileType" required>
<option value="csv">CSV</option>
<option value="mfd">ModForDummies</option>
</select>
</div>
<div class="mb-3">
<label for="fileInput" class="form-label">Upload File</label>
<input type="file" class="form-control" id="fileInput" name="fileInput" accept=".csv, .txt" required>
</div>
<button type="submit" class="btn btn-primary w-100">Submit</button>
</form>
</div>
</div>
</div>
<script src="/assets/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/admin/import.js"></script>
</body>
</html>

View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/assets/css/bootstrap.min.css">
<title>UBS Admin - New ban</title>
</head>
<body class="bg-dark text-light">
<div class="container">
<div class="row justify-content-center mt-5">
<div class="col-md-6 col-lg-4">
<div class="card bg-dark text-light shadow">
<div class="card-body p-4">
<h2 class="text-center mb-4">Import Status</h2>
<div class="alert" id="message" style="display: none;"></div>
<div id="import-status"></div>
</div>
</div>
</div>
<script src="/assets/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/admin/uploadStatus.js"></script>
</body>
</html>