Big one: Add admin panel

This commit is contained in:
Christopher Cookman 2024-12-21 02:14:55 -07:00
parent 127de127a7
commit a28f672516
18 changed files with 1422 additions and 236 deletions

280
index.js
View file

@ -23,12 +23,12 @@ noblox.setCookie(process.env.ROBLOSECURITY)
const mariadb = require('mariadb');
const pool = mariadb.createPool({
host: process.env.DB_HOST, // Replace with your database host
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
database: process.env.DB_DATABASE, // Replace with your database name
connectionLimit: 5 // Adjust connection limit as needed
user: process.env.DB_USER, // Replace with your database username
password: process.env.DB_PASS, // Replace with your database password
database: process.env.DB_DATABASE, // Replace with your database name
connectionLimit: 5 // Adjust connection limit as needed
});
// Express
@ -38,6 +38,12 @@ const port = process.env.SERVER_PORT || 3000;
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
const ejs = require("ejs")
app.set('view engine', 'ejs');
app.set('views', __dirname + '/views');
app.use((req, res, next) => {
if (!process.env.LOGFILE) return next();
var requestIp = req.ip;
@ -59,6 +65,7 @@ const reasonFlagTypes = [
const reasonFlags = flags.defineFlags(reasonFlagTypes)
process.env.REASON_FLAGS = JSON.stringify(reasonFlags)
console.log(process.env.REASON_FLAGS)
// Discord stuff
const Discord = require("discord.js");
@ -94,238 +101,41 @@ client.on("ready", async () => {
})();
});
// In-memory storage for the ban command (store each message id so we can track flags)
const banMessages = {}
client.on("interactionCreate", async (interaction) => {
// Switch by type (either slash command or modal)
switch (interaction.type) {
// Slash Command Handler
case Discord.InteractionType.ApplicationCommand:
if (!interaction.isCommand()) return;
const command = interaction.commandName;
const args = interaction.options;
switch (command) {
// Report Command
case "report":
robloxId = args.getNumber("roblox_id");
reason = args.getString("reason");
// TODO: Report Command
break;
// Ban Command
case "ban":
robloxId = args.getString("roblox_id");
discordId = args.getString("discord_id");
reason = args.getString("reason");
if (robloxId && !robloxId.match(/^\d+$/)) {
return interaction.reply({
ephemeral: true,
content: "Invalid Roblox ID!"
})
}
if (discordId && !discordId.match(/^\d+$/)) {
return interaction.reply({
ephemeral: true,
content: "Invalid Discord ID!"
})
}
if (!robloxId && !discordId) {
return interaction.reply({
ephemeral: true,
content: "Specify a Roblox ID and/or Discord ID!"
})
}
if (robloxId) {
try {
robloxUsername = await noblox.getUsernameFromId(robloxId) || "Unknown"
} catch (e) {
return interaction.reply({
ephemeral: true,
content: "Invalid Roblox ID!"
})
}
} else {
robloxUsername = null
}
if (discordId) {
discordUsername = (await client.users.fetch(discordId))?.username || "Unknown"
} else {
discordUsername = null
}
embed = {
title: "Ban User",
color: 0xff0000,
fields: [
robloxId ? { name: "Roblox", value: `${robloxUsername} (${robloxId})` } : null,
discordId ? { name: "Discord ID", value: `${discordUsername} (${discordId})` } : null,
{ name: "Reason", value: reason},
{ name: "Moderator", value: interaction.user.tag }
].filter(field => field !== null)
}
flagButtons = await reasonFlagTypes.map(flag => {
return new Discord.ButtonBuilder()
.setCustomId(flag)
.setStyle(Discord.ButtonStyle.Danger)
.setLabel(flag)
})
submitButton = new Discord.ButtonBuilder()
.setCustomId("ban")
.setStyle(Discord.ButtonStyle.Primary)
.setLabel("Ban")
.setEmoji("🔨")
interaction.reply({
ephemeral: true,
embeds: [embed],
components: [
new Discord.ActionRowBuilder().addComponents(flagButtons),
new Discord.ActionRowBuilder().addComponents(submitButton)
]
})
rep = await interaction.fetchReply()
banMessages[rep.id] = {
flags: 0,
robloxId,
discordId,
robloxUsername,
discordUsername,
moderator: interaction.user.id,
reason,
interaction: interaction
}
break;
case "unban":
robloxId = args.getNumber("roblox_id");
discordId = args.getString("discord_id");
// In the db, find any instance of either robloxId or discordId and set if the expiry is null or in the future, set it to now
const connection = await pool.getConnection();
try {
await connection.query('UPDATE bans SET expiresTimestamp = UTC_TIMESTAMP() WHERE robloxId = ? OR discordId = ? AND (expiresTimestamp IS NULL OR expiresTimestamp > UTC_TIMESTAMP())', [robloxId || uuid(), discordId || uuid()]);
interaction.reply({
embeds: [
{
title: "User Unbanned",
color: 0x00ff00,
fields: [
robloxId ? { name: "Roblox", value: robloxId } : null,
discordId ? { name: "Discord ID", value: discordId } : null,
{ name: "Moderator", value: interaction.user.tag }
].filter(field => field !== null)
}
]
})
} catch (err) {
log.error(err)
interaction.reply({
embeds: [
{
title: "Error",
color: 0xff0000,
description: "An error occurred while unbanning the user."
}
]
})
} finally {
connection.release();
}
break;
};
break;
// Modal Handler
case Discord.InteractionType.MessageComponent:
if (!interaction.isButton()) return;
const flag = interaction.customId;
const message = banMessages[interaction.message.id];
if (!message) return interaction.reply({
ephemeral: true,
content: "Invalid message!"
})
if (flag == "ban") {
// Ban the user by adding a ban record to the database
const connection = await pool.getConnection();
try {
await connection.query('INSERT INTO bans (robloxId, discordId, robloxUsername, discordUsername, reasonShort, moderator, reasonsFlag) VALUES (?, ?, ?, ?, ?, ? ,?)', [message.robloxId, message.discordId, message.robloxUsername, message.discordUsername, message.reason, message.moderator, message.flags]);
message.interaction.editReply({
embeds: [
{
title: "User Banned",
color: 0xff0000,
fields: [
message.robloxId ? { name: "Roblox", value: `${message.robloxUsername} (${message.robloxId})` } : null,
message.discordId ? { name: "Discord ID", value: `${message.discordUsername} (${message.discordId})` }: null,
{ name: "Moderator", value: interaction.user.tag }
].filter(field => field !== null)
}
],
components: []
})
} catch (err) {
log.error(err)
message.interaction.editReply({
embeds: [
{
title: "Error",
color: 0xff0000,
description: "An error occurred while banning the user."
}
],
components: []
})
} finally {
connection.release();
}
} else {
message.flags ^= reasonFlags[flag]
interaction.deferUpdate();
flagButtons = await reasonFlagTypes.map(flag => {
return new Discord.ButtonBuilder()
.setCustomId(flag)
.setStyle(flags.hasFlag(message.flags, reasonFlags[flag]) ? Discord.ButtonStyle.Success : Discord.ButtonStyle.Danger)
.setLabel(flag)
})
submitButton = new Discord.ButtonBuilder()
.setCustomId("ban")
.setStyle(Discord.ButtonStyle.Primary)
.setLabel("Ban")
.setEmoji("🔨")
message.interaction.editReply({
components: [
new Discord.ActionRowBuilder().addComponents(flagButtons),
new Discord.ActionRowBuilder().addComponents(submitButton)
]
})
}
break;
}
});
// Startup
log.info("Starting up...")
const bcrypt = require("bcrypt")
const crypto = require("crypto")
pool.getConnection().then((conn) => {
require("./migrations")(pool).then(() => {
conn.query("SELECT * FROM users WHERE id = 1").then((row) => {
if (row.length == 0 || process.env.RESET_ADMIN == "true") {
// delete all users (The big scary one lol)
conn.query("DELETE FROM users").then(() => {
// Generate 32 char random string
const passwd = process.env.DEV_PWD || crypto.randomBytes(32).toString('hex');
bcrypt.hash(passwd, 10).then((hash) => {
conn.query("INSERT INTO users (id, username, passwordHash) VALUES (1, 'admin', ?)",
[hash]).then(() => {
console.log(`Created admin user with password: ${passwd}`);
});
});
});
}
});
}).finally(() => {
conn.release();
// Load all route modules from the 'routes' folder
const routesPath = path.join(__dirname, 'routes');
fs.readdirSync(routesPath).forEach((file) => {
const route = require(path.join(routesPath, file));
const routeName = `/${file.replace('.js', '')}`; // Use filename as route base
app.use(routeName, route);
log.info(`Using ${routeName}`)
});
require("./migrations")(pool).then(() => {
// Load all route modules from the 'routes' folder
const routesPath = path.join(__dirname, 'routes');
fs.readdirSync(routesPath).forEach((file) => {
const route = require(path.join(routesPath, file));
const routeName = `/${file.replace('.js', '')}`; // Use filename as route base
app.use(routeName, route);
log.info(`Using ${routeName}`)
app.listen(port, () => {
log.info(`Listening on ${port}`)
})
client.login(process.env.DISCORD_TOKEN);
});
app.listen(port, () => {
log.info(`Listening on ${port}`)
})
client.login(process.env.DISCORD_TOKEN);
});

View file

@ -59,7 +59,7 @@ function runMigrations(pool) {
resolve();
})
.catch(err => {
console.error('Error running migrations:', err);
console.errorr('Error running migrations:', err);
reject(err);
})
.finally(() => {

View file

@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(255) NOT NULL,
passwordHash VARCHAR(255) NOT NULL
);

View file

@ -0,0 +1,2 @@
ALTER TABLE users
ADD COLUMN totp_token VARCHAR(255);

632
package-lock.json generated
View file

@ -9,13 +9,16 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.1.1",
"colors": "^1.4.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",
"noblox.js": "^6.0.2",
"totp-generator": "^1.0.0",
"uuid": "^11.0.3"
}
},
@ -170,6 +173,26 @@
"integrity": "sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==",
"license": "MIT"
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
"license": "BSD-3-Clause",
"dependencies": {
"detect-libc": "^2.0.0",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.7",
"nopt": "^5.0.0",
"npmlog": "^5.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tar": "^6.1.11"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@microsoft/signalr": {
"version": "8.0.7",
"resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-8.0.7.tgz",
@ -312,6 +335,12 @@
"npm": ">=7.0.0"
}
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"license": "ISC"
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@ -337,6 +366,41 @@
"node": ">= 0.6"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/agent-base/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/agent-base/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -353,6 +417,15 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -368,6 +441,26 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
"license": "ISC"
},
"node_modules/are-we-there-yet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@ -445,6 +538,20 @@
],
"license": "MIT"
},
"node_modules/bcrypt": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
"integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",
"node-addon-api": "^5.0.0"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
@ -597,6 +704,15 @@
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -615,6 +731,15 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"license": "ISC",
"bin": {
"color-support": "bin.js"
}
},
"node_modules/colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
@ -642,6 +767,12 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"license": "MIT"
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"license": "ISC"
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -759,6 +890,12 @@
"node": ">=0.4.0"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"license": "MIT"
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@ -787,6 +924,15 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/discord-api-types": {
"version": "0.37.100",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.100.tgz",
@ -929,6 +1075,12 @@
"node": ">=0.10.0"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
@ -1071,6 +1223,40 @@
"node": ">= 0.10.0"
}
},
"node_modules/express-session": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz",
"integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==",
"license": "MIT",
"dependencies": {
"cookie": "0.7.2",
"cookie-signature": "1.0.7",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.1",
"uid-safe": "~2.1.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/express-session/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express-session/node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@ -1195,6 +1381,36 @@
"node": ">= 0.6"
}
},
"node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"license": "ISC",
"dependencies": {
"minipass": "^3.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/fs-minipass/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"license": "ISC"
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -1204,6 +1420,27 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"aproba": "^1.0.3 || ^2.0.0",
"color-support": "^1.1.2",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.1",
"object-assign": "^4.1.1",
"signal-exit": "^3.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wide-align": "^1.1.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
@ -1232,6 +1469,27 @@
"assert-plus": "^1.0.0"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@ -1315,6 +1573,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"license": "ISC"
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@ -1388,6 +1652,42 @@
"node": ">=0.10"
}
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/https-proxy-agent/node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/https-proxy-agent/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -1400,6 +1700,17 @@
"node": ">=0.10.0"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@ -1415,6 +1726,15 @@
"node": ">= 0.10"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
@ -1500,6 +1820,15 @@
"verror": "1.10.0"
}
},
"node_modules/jssha": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz",
"integrity": "sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==",
"license": "BSD-3-Clause",
"engines": {
"node": "*"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@ -1524,6 +1853,30 @@
"integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==",
"license": "MIT"
},
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"license": "MIT",
"dependencies": {
"semver": "^6.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/make-dir/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/mariadb": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.4.0.tgz",
@ -1624,6 +1977,52 @@
"node": "*"
}
},
"node_modules/minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
"license": "ISC",
"engines": {
"node": ">=8"
}
},
"node_modules/minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"license": "MIT",
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/minizlib/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"license": "MIT",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -1657,6 +2056,12 @@
"node": ">=18.18"
}
},
"node_modules/node-addon-api": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@ -1677,6 +2082,34 @@
}
}
},
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"license": "ISC",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"are-we-there-yet": "^2.0.0",
"console-control-strings": "^1.1.0",
"gauge": "^3.0.0",
"set-blocking": "^2.0.0"
}
},
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
@ -1698,6 +2131,15 @@
"node": "*"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
@ -1722,6 +2164,24 @@
"node": ">= 0.8"
}
},
"node_modules/on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/parse5": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
@ -1780,6 +2240,15 @@
"node": ">= 0.8"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
@ -1898,6 +2367,15 @@
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"license": "MIT"
},
"node_modules/random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -1922,12 +2400,42 @@
"node": ">= 0.8"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"license": "MIT"
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"license": "ISC",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -1954,6 +2462,18 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/send": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
@ -2008,6 +2528,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
@ -2055,6 +2581,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC"
},
"node_modules/sshpk": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
@ -2098,6 +2630,41 @@
"bluebird": "^2.6.2"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -2110,6 +2677,23 @@
"node": ">=8"
}
},
"node_modules/tar": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"license": "ISC",
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
"minipass": "^5.0.0",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@ -2119,6 +2703,15 @@
"node": ">=0.6"
}
},
"node_modules/totp-generator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/totp-generator/-/totp-generator-1.0.0.tgz",
"integrity": "sha512-Iu/1Lk60/MH8FE+5cDWPiGbwKK1hxzSq+KT9oSqhZ1BEczGIKGcN50bP0WMLiIZKRg7t29iWLxw6f81TICQdoA==",
"license": "MIT",
"dependencies": {
"jssha": "^3.3.1"
}
},
"node_modules/tough-cookie": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
@ -2171,6 +2764,18 @@
"node": ">= 0.6"
}
},
"node_modules/uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"license": "MIT",
"dependencies": {
"random-bytes": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/undici": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz",
@ -2223,6 +2828,12 @@
"requires-port": "^1.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@ -2317,6 +2928,21 @@
"webidl-conversions": "^3.0.0"
}
},
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"license": "ISC",
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
@ -2337,6 +2963,12 @@
"optional": true
}
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
}
}
}

View file

@ -10,13 +10,16 @@
"license": "ISC",
"description": "",
"dependencies": {
"bcrypt": "^5.1.1",
"colors": "^1.4.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",
"noblox.js": "^6.0.2",
"totp-generator": "^1.0.0",
"uuid": "^11.0.3"
}
}

6
public/assets/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,66 @@
const alertBox = (type, message) => {
const alert = document.getElementById('message');
alert.classList.add(type);
alert.innerText = message;
alert.style.display = 'block';
};
var reasonFlags = {};
fetch('/api/v1/info')
.then(response => response.json())
.then(data => {
reasonFlags = data.reasonFlags;
})
.catch(error => {
console.error('Error:', error);
});
document.getElementById('banForm').addEventListener('submit', function (event) {
event.preventDefault();
// Combine flag values
const checkboxes = document.querySelectorAll('.flag-checkbox');
let combinedFlags = 0;
checkboxes.forEach(checkbox => {
if (checkbox.checked) {
combinedFlags |= parseInt(checkbox.value);
}
});
document.getElementById('reasonsFlag').value = combinedFlags;
const data = Object.fromEntries(new FormData(event.target));
if (data.expires) {
const expiresDate = new Date(data.expiresTimestamp);
data.expiresTimestamp = Math.floor(expiresDate.getTime());
}
if ((!data.robloxId && !data.discordId)) {
alertBox('alert-danger', 'Please enter a Roblox ID or Discord ID.');
return;
}
fetch(event.target.action, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
console.log('Success:', result);
// Handle success (e.g., show a success message, redirect, etc.)
window.location.href = '/admin';
} else {
console.error('Error:', result.message);
alertBox('alert-danger', 'An error has occurred: ' + result.message);
}
})
.catch(error => {
console.error('Error:', error);
alertBox('alert-danger', 'An error has occurred. Please contact support.\n' + error);
// Handle error (e.g., show an error message)
});
});

View file

@ -0,0 +1,43 @@
var reasonFlags = {};
fetch('/api/v1/info')
.then(response => response.json())
.then(data => {
reasonFlags = data.reasonFlags;
console.log('Reason Flags:', reasonFlags);
fetch('/admin/api/bans')
.then(response => response.json())
.then(data => {
const tableBody = document.querySelector('#bansTableBody');
data.forEach(ban => {
const row = document.createElement('tr');
const banTimestamp = new Date(ban.banTimestamp).toLocaleString();
const expiresTimestamp = ban.expiresTimestamp ? new Date(ban.expiresTimestamp).toLocaleString() : 'Never';
const reasonsFlagNames = getSetFlags(ban.reasonsFlag, reasonFlags).join(', ');
console.log(ban.reasonsFlag)
console.log(reasonFlags)
console.log(getSetFlags(ban.reasonsFlag, reasonFlags))
row.innerHTML = `
<td>${ban.id}</td>
<td>${ban.robloxId || 'N/A'}</td>
<td>${ban.discordId || 'N/A'}</td>
<td>${ban.robloxUsername || 'N/A'}</td>
<td>${ban.discordUsername || 'N/A'}</td>
<td>${ban.reasonShort || 'N/A'}</td>
<td>${ban.reasonLong || 'N/A'}</td>
<td>${reasonsFlagNames}</td>
<td>${ban.moderator || 'N/A'}</td>
<td>${banTimestamp}</td>
<td>${expiresTimestamp}</td>
<td style="color: ${ban.expiresTimestamp && new Date(ban.expiresTimestamp) < new Date() ? 'green' : ''};">
${expiresTimestamp}
</td>
<td>
<a href="/admin/edit/${ban.id}" class="btn btn-primary btn-sm">Edit</a>
</td>
`;
tableBody.appendChild(row);
});
})
.catch(error => console.error('Error fetching ban data:', error));
})
.catch(error => console.error('Error fetching reason flags:', error));

View file

@ -0,0 +1,72 @@
const alertBox = (type, message) => {
const alert = document.getElementById('message');
alert.classList.add(type);
alert.innerText = message;
alert.style.display = 'block';
};
// Set expiresTimestamp to the time in const ban.expiresTimestamp (convert from js date number to local time string that conforms with html form values)
const expiresTimestamp = document.getElementById('expiresTimestamp').value;
const expiresDate = new Date(ban.expiresTimestamp)
document.getElementById('expiresTimestamp').value = new Date(expiresDate.getTime() - (expiresDate.getTimezoneOffset() * 60000)).toISOString().slice(0, 16);
var reasonFlags = {};
fetch('/api/v1/info')
.then(response => response.json())
.then(data => {
reasonFlags = data.reasonFlags;
})
.catch(error => {
console.error('Error:', error);
});
document.getElementById('banForm').addEventListener('submit', function (event) {
event.preventDefault();
// Combine flag values
const checkboxes = document.querySelectorAll('.flag-checkbox');
let combinedFlags = 0;
checkboxes.forEach(checkbox => {
if (checkbox.checked) {
combinedFlags |= parseInt(checkbox.value);
}
});
document.getElementById('reasonsFlag').value = combinedFlags;
const data = Object.fromEntries(new FormData(event.target));
if (data.expires) {
const expiresDate = new Date(data.expiresTimestamp);
data.expiresTimestamp = Math.floor(expiresDate.getTime());
}
if ((!data.robloxId && !data.discordId)) {
alertBox('alert-danger', 'Please enter a Roblox ID or Discord ID.');
return;
}
fetch(event.target.action, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
console.log('Success:', result);
// Handle success (e.g., show a success message, redirect, etc.)
window.location.href = '/admin';
} else {
console.error('Error:', result.message);
alertBox('alert-danger', 'An error has occurred: ' + result.message);
}
})
.catch(error => {
console.error('Error:', error);
alertBox('alert-danger', 'An error has occurred. Please contact support.\n' + error);
// Handle error (e.g., show an error message)
});
});

File diff suppressed because one or more lines are too long

83
public/assets/js/flags.js Normal file
View file

@ -0,0 +1,83 @@
/**
* Adds a flag to the current flags.
* @param {number} flags - The current set of flags.
* @param {number} flagToAdd - The flag to add.
* @returns {number} - The updated flags with the new flag added.
*/
function addFlag(flags, flagToAdd) {
return flags | flagToAdd;
}
/**
* Removes a flag from the current flags.
* @param {number} flags - The current set of flags.
* @param {number} flagToRemove - The flag to remove.
* @returns {number} - The updated flags with the flag removed.
*/
function removeFlag(flags, flagToRemove) {
return flags & ~flagToRemove;
}
/**
* Checks if a flag is set in the current flags.
* @param {number} flags - The current set of flags.
* @param {number} flagToCheck - The flag to check.
* @returns {boolean} - True if the flag is set, otherwise false.
*/
function hasFlag(flags, flagToCheck) {
return (flags & flagToCheck) !== 0;
}
/**
* Toggles a flag in the current flags.
* @param {number} flags - The current set of flags.
* @param {number} flagToToggle - The flag to toggle.
* @returns {number} - The updated flags with the flag toggled.
*/
function toggleFlag(flags, flagToToggle) {
return flags ^ flagToToggle;
}
/**
* Checks if all specified flags are set.
* @param {number} flags - The current set of flags.
* @param {number} flagsToCheck - The flags to check (can be multiple combined).
* @returns {boolean} - True if all specified flags are set, otherwise false.
*/
function hasAllFlags(flags, flagsToCheck) {
return (flags & flagsToCheck) === flagsToCheck;
}
/**
* Checks if any of the specified flags are set.
* @param {number} flags - The current set of flags.
* @param {number} flagsToCheck - The flags to check (can be multiple combined).
* @returns {boolean} - True if any of the specified flags are set, otherwise false.
*/
function hasAnyFlag(flags, flagsToCheck) {
return (flags & flagsToCheck) !== 0;
}
/**
* Generates a flag mapping from an array of names.
* @param {string[]} flagNames - An array of names to define as flags.
* @returns {Object} - An object where keys are the flag names and values are powers of 2.
*/
function defineFlags(flagNames) {
return flagNames.reduce((flags, name, index) => {
flags[name] = 1 << index; // Assign a power of 2 to each flag
return flags;
}, {});
}
/**
* Retrieves the names of the flags that are set.
* @param {number} flags - The current set of flags.
* @param {Object} flagDefinitions - An object where keys are flag names and values are powers of 2.
* @returns {string[]} - An array of flag names that are set.
*/
function getSetFlags(flags, flagDefinitions) {
return Object.keys(flagDefinitions).filter(flagName =>
(flags & flagDefinitions[flagName]) !== 0
);
}

46
public/assets/js/login.js Normal file
View file

@ -0,0 +1,46 @@
const alertBox = (type, message) => {
const alert = document.getElementById('message');
alert.classList.add(type);
alert.innerText = message;
alert.style.display = 'block';
};
document.getElementById('loginForm').addEventListener('submit', function (event) {
event.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const totp = document.getElementById('totp').value;
const body = { username, password };
if (document.getElementById('totpField').style.display !== 'none') {
body.totp = totp;
}
fetch(window.location.href, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
})
.then(response => response.json())
.then(data => {
console.log(data)
if (data.success) {
// Handle successful login
window.location.href = data.redirect || window.location.href.split('/').slice(0, -1).join('/');
} else if (data.totpRequired) {
// Prompt for TOTP
document.getElementById('totpField').style.display = 'block';
alertBox('alert-danger', data.message);
} else {
// Show error modal
alertBox('alert-danger', data.message);
}
})
.catch(error => {
console.error('Error:', error);
alertBox('alert-danger', 'An error has occurred. Please try again later.');
});
});

177
routes/admin.js Normal file
View file

@ -0,0 +1,177 @@
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;
// 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
database: process.env.DB_DATABASE, // Replace with your database name
connectionLimit: 5 // Adjust connection limit as needed
});
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();
}
// 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;
await conn.query('INSERT INTO bans (reasonShort, reasonLong, reasonsFlag, moderator, expiresTimestamp, robloxId, discordId) VALUES (?, ?, ?, ?, ?, ?, ?)',
[reasonShort, reasonLong, reasonsFlag, moderator, expiresTimestamp, robloxId, discordId]);
conn.end();
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;
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;
await conn.query('UPDATE bans SET reasonShort = ?, reasonLong = ?, reasonsFlag = ?, moderator = ?, expiresTimestamp = ?, robloxId = ?, discordId = ? WHERE id = ?',
[reasonShort, reasonLong, reasonsFlag, moderator, expiresTimestamp, robloxId, discordId, id]);
conn.end();
res.json({ success: true, message: 'User updated successfully', redirect: '/admin' });
});
// 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);
});
// 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;

67
views/admin/create.ejs Normal file
View file

@ -0,0 +1,67 @@
<!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">Ban User</h2>
<div class="alert" id="message" style="display: none;"></div>
<form id="banForm">
<div class="mb-3">
<label for="robloxId" class="form-label">Roblox ID (Optional)</label>
<input type="text" class="form-control" id="robloxId" name="robloxId" pattern="\d*" title="ID Only, must be a number">
</div>
<div class="mb-3">
<label for="discordId" class="form-label">Discord ID (Optional)</label>
<input type="text" class="form-control" id="discordId" name="discordId" pattern="\d*" title="ID Only, must be a number">
</div>
<div class="mb-3">
<label for="robloxUsername" class="form-label">Roblox Username (Optional)</label>
<input type="text" class="form-control" id="robloxUsername" name="robloxUsername">
</div>
<div class="mb-3">
<label for="discordUsername" class="form-label">Discord Username (Optional)</label>
<input type="text" class="form-control" id="discordUsername" name="discordUsername">
</div>
<div class="mb-3">
<label for="reasonShort" class="form-label">Short Reason</label>
<input type="text" class="form-control" id="reasonShort" name="reasonShort" required>
</div>
<div class="mb-3">
<label for="reasonLong" class="form-label">Long Reason</label>
<textarea class="form-control" id="reasonLong" name="reasonLong" rows="3" required></textarea>
</div>
<div class="mb-3">
<label class="form-label">Flags</label>
<div>
<input type="hidden" id="reasonsFlag" name="reasonsFlag" value="0">
<% Object.keys(reasonFlags).forEach(function(key) { %>
<div class="form-check">
<input class="form-check-input flag-checkbox" type="checkbox" id="<%= key %>" value="<%= reasonFlags[key] %>">
<label class="form-check-label" for="<%= key %>"><%= key %></label>
</div>
<% }); %>
</div>
</div>
<div class="mb-3">
<label for="expiresTimestamp" class="form-label">Expires (Optional)</label>
<input type="datetime-local" class="form-control" id="expiresTimestamp" name="expiresTimestamp">
</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/create.js"></script>
</body>
</html>

56
views/admin/dashboard.ejs Normal file
View file

@ -0,0 +1,56 @@
<!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 Dashboard</title>
</head>
<body class="bg-dark text-light">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">UBS Admin</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="#">Welcome, <%= session.user.username %></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/logout">Logout</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container mt-5">
<h2 class="mb-4">Bans</h2>
<table class="table table-dark table-striped">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Roblox ID</th>
<th scope="col">Discord ID</th>
<th scope="col">Roblox Username</th>
<th scope="col">Discord Username</th>
<th scope="col">Reason (Short)</th>
<th scope="col">Reason (Long)</th>
<th scope="col">Reasons Flag</th>
<th scope="col">Moderator</th>
<th scope="col">Ban Timestamp</th>
<th scope="col">Expires Timestamp</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody id="bansTableBody">
<!-- Rows will be populated by the script -->
</tbody>
</table>
</div>
<script src="/assets/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/flags.js"></script>
<script src="/assets/js/admin/dashboard.js"></script>
</body>
</html>

72
views/admin/edit.ejs Normal file
View file

@ -0,0 +1,72 @@
<!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 - Edit 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">Edit Ban</h2>
<div class="alert" id="message" style="display: none;"></div>
<form id="banForm">
<div class="mb-3">
<label for="robloxId" class="form-label">Roblox ID (Optional)</label>
<input type="text" class="form-control" id="robloxId" name="robloxId" pattern="\d*" title="ID Only, must be a number" value="<%= ban.robloxId %>">
</div>
<div class="mb-3">
<label for="discordId" class="form-label">Discord ID (Optional)</label>
<input type="text" class="form-control" id="discordId" name="discordId" pattern="\d*" title="ID Only, must be a number" value="<%= ban.discordId %>">
</div>
<div class="mb-3">
<label for="robloxUsername" class="form-label">Roblox Username (Optional)</label>
<input type="text" class="form-control" id="robloxUsername" name="robloxUsername" value="<%= ban.robloxUsername %>">
</div>
<div class="mb-3">
<label for="discordUsername" class="form-label">Discord Username (Optional)</label>
<input type="text" class="form-control" id="discordUsername" name="discordUsername" value="<%= ban.discordUsername %>">
</div>
<div class="mb-3">
<label for="reasonShort" class="form-label">Short Reason</label>
<input type="text" class="form-control" id="reasonShort" name="reasonShort" required value="<%= ban.reasonShort %>">
</div>
<div class="mb-3">
<label for="reasonLong" class="form-label">Long Reason</label>
<textarea class="form-control" id="reasonLong" name="reasonLong" rows="3" required><%= ban.reasonLong %></textarea>
</div>
<div class="mb-3">
<label class="form-label">Flags</label>
<div>
<input type="hidden" id="reasonsFlag" name="reasonsFlag" value="0">
<% Object.keys(reasonFlags).forEach(function(key) { %>
<div class="form-check">
<input class="form-check-input flag-checkbox" type="checkbox" id="<%= key %>" value="<%= reasonFlags[key] %>" <%= setFlags.includes(key) ? 'checked' : '' %>>
<label class="form-check-label" for="<%= key %>"><%= key %></label>
</div>
<% }); %>
</div>
</div>
<div class="mb-3">
<label for="expiresTimestamp" class="form-label">Expires (Optional)</label>
<input type="datetime-local" class="form-control" id="expiresTimestamp" name="expiresTimestamp" value="<%= ban.expiresTimestamp %>">
</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>
const ban = <%- JSON.stringify(ban) %>;
console.log(ban)
</script>
<script src="/assets/js/flags.js"></script>
<script src="/assets/js/admin/edit.js"></script>
</body>
</html>

39
views/admin/login.ejs Normal file
View file

@ -0,0 +1,39 @@
<!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 Login</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">UBS Admin</h2>
<div class="alert" id="message" style="display: none;"></div>
<form id="loginForm">
<div class="mb-3">
<label for="username" class="form-label">Username:</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password:</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3" id="totpField" style="display: none;">
<label for="totp" class="form-label">2FA Key:</label>
<input type="text" class="form-control" id="totp" name="totp" pattern="\d{6}" maxlength="6" title="Please enter a 6-digit number">
</div>
<button type="submit" class="btn btn-primary w-100">Login</button>
</form>
</div>
</div>
</div>
<script src="/assets/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/login.js"></script>
</body>
</html>