I’ll just paste the entire dashboard.js file here. I think it’s easier. (Btw, this is not something I’ve done. [And the huge comments aren’t mine.])
/*
DASHBOARD EXAMPLE
This is a very simple dashboard example, but even in its simple state, there are still a
lot of moving parts working together to make this a reality. I shall attempt to explain
those parts in as much details as possible, but be aware: there's still a lot of complexity
and you shouldn't expect to really understand all of it instantly.
Pay attention, be aware of the details, and read the comments.
Note that this *could* be split into multiple files, but for the purpose of this
example, putting it in one file is a little simpler. Just *a little*.
*/
// quick.db
const db = require('quick.db')
// Native Node Imports
const url = require("url");
const path = require("path");
const fs = require("fs");
// Used for Permission Resolving...
const Discord = require("discord.js");
// Express Session
const express = require("express");
const app = express();
// Express Plugins
// Specifically, passport helps with oauth2 in general.
// passport-discord is a plugin for passport that handles Discord's specific implementation.
const passport = require("passport");
const session = require("express-session");
const Strategy = require("passport-discord").Strategy;
// Helmet is a security plugin
//const helmet = require('helmet');
// Used to parse Markdown from things like ExtendedHelp
const md = require("marked");
// For logging
const morgan = require("morgan");
// For stats
const moment = require("moment");
require("moment-duration-format");
module.exports = client => {
if (client.config.dashboard.enabled !== "true")
return client.log("log", "Dashboard disabled", "INFO");
// It's easier to deal with complex paths.
// This resolves to: yourbotdir/dashboard/
const dataDir = path.resolve(`${process.cwd()}${path.sep}dashboard`);
// This resolves to: yourbotdir/dashboard/templates/
// which is the folder that stores all the internal template files.
const templateDir = path.resolve(`${dataDir}${path.sep}templates`);
app.set("trust proxy", 5); // Proxy support
// The public data directory, which is accessible from the *browser*.
// It contains all css, client javascript, and images needed for the site.
app.use(
"/public",
express.static(path.resolve(`${dataDir}${path.sep}public`), {
maxAge: "10d"
})
);
app.use(morgan("combined")); // Logger
// uhhhh check what these do.
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((obj, done) => {
done(null, obj);
});
/*
This defines the **Passport** oauth2 data. A few things are necessary here.
clientID = Your bot's client ID, at the top of your app page. Please note,
older bots have BOTH a client ID and a Bot ID. Use the Client one.
clientSecret: The secret code at the top of the app page that you have to
click to reveal. Yes that one we told you you'd never use.
callbackURL: The URL that will be called after the login. This URL must be
available from your PC for now, but must be available publically if you're
ever to use this dashboard in an actual bot.
scope: The data scopes we need for data. identify and guilds are sufficient
for most purposes. You might have to add more if you want access to more
stuff from the user. See: https://discordapp.com/developers/docs/topics/oauth2
See config.js.example to set these up.
*/
var protocol;
if (client.config.dashboard.secure === "true") {
client.protocol = "https://";
} else {
client.protocol = "http://";
}
protocol = client.protocol;
client.callbackURL = `${protocol}${client.config.dashboard.domain}/callback`;
client.log("log", `Callback URL: ${client.callbackURL}`, "INFO");
passport.use(
new Strategy(
{
clientID: client.appInfo.id,
clientSecret: client.config.dashboard.oauthSecret,
callbackURL: client.callbackURL,
scope: ["identify", "guilds"]
},
(accessToken, refreshToken, profile, done) => {
process.nextTick(() => done(null, profile));
}
)
);
// Session data, used for temporary storage of your visitor's session information.
// the `secret` is in fact a 'salt' for the data, and should not be shared publicly.
app.use(
session({
secret: client.config.dashboard.sessionSecret,
resave: false,
saveUninitialized: false
})
);
// Initializes passport and session.
app.use(passport.initialize());
app.use(passport.session());
// The domain name used in various endpoints to link between pages.
app.locals.domain = client.config.dashboard.domain;
// The EJS templating engine gives us more power
app.engine("html", require("ejs").renderFile);
app.set("view engine", "html");
// body-parser reads incoming JSON or FORM data and simplifies their
// use in code.
var bodyParser = require("body-parser");
app.use(bodyParser.json()); // to support JSON-encoded bodies
app.use(
bodyParser.urlencoded({
// to support URL-encoded bodies
extended: true
})
);
/*
Authentication Checks. checkAuth verifies regular authentication,
whereas checkAdmin verifies the bot owner. Those are used in url
endpoints to give specific permissions.
*/
function checkAuth(req, res, next) {
if (req.isAuthenticated()) return next();
req.session.backURL = req.url;
res.redirect("/login");
}
function cAuth(req, res) {
if (req.isAuthenticated()) return;
req.session.backURL = req.url;
res.redirect("/login");
}
function checkAdmin(req, res, next) {
if (req.isAuthenticated() && req.user.id === client.config.ownerID || req.isAuthenticated() && req.user.id === client.config.subownerID)
return next();
req.session.backURL = req.originalURL;
res.redirect("/");
}
var privacyMD = "";
fs.readFile(
`${process.cwd()}${path.sep}dashboard${path.sep}public${
path.sep
}PRIVACY.md`,
function(err, data) {
if (err) {
console.log(err);
privacyMD = "Error";
return;
}
privacyMD = data
.toString()
.replace(/\{\{botName\}\}/g, client.user.username)
.replace(
/\{\{email\}\}/g,
client.config.dashboard.legalTemplates.contactEmail
);
if (client.config.dashboard.secure !== "true") {
privacyMD = privacyMD.replace(
"Sensitive and private data exchange between the Site and its Users happens over a SSL secured communication channel and is encrypted and protected with digital signatures.",
""
);
}
}
);
var termsMD = "";
fs.readFile(
`${process.cwd()}${path.sep}dashboard${path.sep}public${path.sep}TERMS.md`,
function(err, data) {
if (err) {
console.log(err);
privacyMD = "Error";
return;
}
termsMD = data
.toString()
.replace(/\{\{botName\}\}/g, client.user.username)
.replace(
/\{\{email\}\}/g,
client.config.dashboard.legalTemplates.contactEmail
);
}
);
// Index page. If the user is authenticated, it shows their info
// at the top right of the screen.
app.get("/", (req, res) => {
if (req.isAuthenticated()) {
res.render(path.resolve(`${templateDir}${path.sep}index.ejs`), {
bot: client,
auth: true,
user: req.user
});
} else {
res.render(path.resolve(`${templateDir}${path.sep}index.ejs`), {
bot: client,
auth: false,
user: null
});
}
});
app.get("/stats", (req, res) => {
if (client.config.dashboard.protectStats === "true") {
cAuth(req, res);
}
const duration = moment
.duration(client.uptime)
.format(" D [days], H [hrs], m [mins], s [secs]");
//const members = client.guilds.reduce((p, c) => p + c.memberCount, 0);
const members = `${client.users.filter(u => u.id !== "1").size} (${
client.users.filter(u => u.id !== "1").filter(u => u.bot).size
} bots)`;
const textChannels = client.channels.filter(c => c.type === "text").size;
const voiceChannels = client.channels.filter(c => c.type === "voice").size;
const guilds = client.guilds.size;
res.render(path.resolve(`${templateDir}${path.sep}stats.ejs`), {
bot: client,
auth: req.isAuthenticated() ? true : false,
user: req.isAuthenticated() ? req.user : null,
stats: {
servers: guilds,
members: members,
text: textChannels,
voice: voiceChannels,
uptime: duration,
commands: client.commandsNumber,
memoryUsage: (process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2),
dVersion: Discord.version,
nVersion: process.version,
bVersion: client.version
}
});
});
app.get("/legal", function(req, res) {
md.setOptions({
renderer: new md.Renderer(),
gfm: true,
tables: true,
breaks: false,
pedantic: false,
sanitize: false,
smartLists: true,
smartypants: false
});
/*var showdown = require('showdown');
var converter = new showdown.Converter(),
textPr = privacyMD,
htmlPr = converter.makeHtml(textPr),
textTe = termsMD,
htmlTe = converter.makeHtml(textTe);
res.render(path.resolve(`${templateDir}${path.sep}legal.ejs`), {
bot: client,
auth: req.isAuthenticated() ? true : false,
user: req.isAuthenticated() ? req.user : null,
privacy: htmlPr.replace(/\\'/g, `'`),
terms: htmlTe.replace(/\\'/g, `'`),
edited: client.config.dashboard.legalTemplates.lastEdited
});*/
res.render(path.resolve(`${templateDir}${path.sep}legal.ejs`), {
bot: client,
auth: req.isAuthenticated() ? true : false,
user: req.isAuthenticated() ? req.user : null,
privacy: md(privacyMD),
terms: md(termsMD),
edited: client.config.dashboard.legalTemplates.lastEdited
});
});
// The login page saves the page the person was on in the session,
// then throws the user to the Discord OAuth2 login page.
app.get(
"/login",
(req, res, next) => {
if (req.session.backURL) {
req.session.backURL = req.session.backURL;
} else if (req.headers.referer) {
const parsed = url.parse(req.headers.referer);
if (parsed.hostname === app.locals.domain) {
req.session.backURL = parsed.path;
}
} else {
req.session.backURL = "/";
}
next();
},
passport.authenticate("discord")
);
app.get(
"/callback",
passport.authenticate("discord", {
failureRedirect: "/"
}),
(req, res) => {
if (req.session.backURL) {
res.redirect(req.session.backURL);
req.session.backURL = null;
} else {
res.redirect("/");
}
}
);
app.get("/admin", checkAdmin, (req, res) => {
res.render(path.resolve(`${templateDir}${path.sep}admin.ejs`), {
bot: client,
user: req.user,
auth: true
});
});
app.get("/dashboard", checkAuth, (req, res) => {
const perms = Discord.EvaluatedPermissions;
res.render(path.resolve(`${templateDir}${path.sep}dashboard.ejs`), {
perms: perms,
bot: client,
user: req.user,
auth: true
});
});
app.get("/add/:guildID", checkAuth, (req, res) => {
req.session.backURL = "/dashboard";
var invitePerm = client.config.dashboard.invitePerm;
var inviteURL = `https://discordapp.com/oauth2/authorize?client_id=${
client.appInfo.id
}&scope=bot&guild_id=${
req.params.guildID
}&response_type=code&redirect_uri=${encodeURIComponent(
`${client.callbackURL}`
)}&permissions=${invitePerm}`;
if (client.guilds.has(req.params.guildID)) {
res.send(
'<p>The bot is already there... <script>setTimeout(function () { window.location="/dashboard"; }, 1000);</script><noscript><meta http-equiv="refresh" content="1; url=/dashboard" /></noscript>'
);
} else {
res.redirect(inviteURL);
}
});
app.post("/manage/:guildID", checkAuth, (req, res) => {
const guild = client.guilds.get(req.params.guildID);
if (!guild) return res.status(404);
const isManaged =
guild && !!guild.member(req.user.id)
? guild.member(req.user.id).permissions.has("MANAGE_GUILD")
: false;
if (req.user.id === client.config.ownerID || req.user.id === client.config.subownerID) {
console.log(`Admin bypass for managing server: ${req.params.guildID}`);
} else if (!isManaged) {
res.redirect("/");
}
const settings = client.settings.get(guild.id);
for (const key in settings) {
var value = req.body[key];
//console.log(typeof value);
//console.log(value);
/*if (value.length > 1) {
for (var i = 0; i < value.length; i++) {
console.log(value[i]);
value[i] = value[i].replace(',', '');
console.log(value[i]);
}
} else {*/
if (value.indexOf(",") > -1) {
settings[key] = value.split(",");
//console.log('S: ' + settings[key]);
//console.log(typeof settings[key]);
//console.log('Split');
//console.log(typeof value);
//console.log(value);
} else if (key === "inviteWhitelist") {
var iWArray = [];
value = value.replace(/\s/g, "");
value.indexOf(",") > -1
? (iWArray = value.split(","))
: iWArray.push(value);
settings[key] = iWArray;
}
if (key === "swearWords") {
var sWArray = [];
value = value.replace(/\s/g, "");
value.indexOf(",") > -1
? (sWArray = value.split(","))
: sWArray.push(value);
settings[key] = sWArray;
} else {
settings[key] = value;
//console.log(typeof value);
//console.log(value);
}
//settings[key] = req.body[key];
}
client.settings.set(guild.id, settings);
res.redirect(`/manage/${req.params.guildID}`);
});
app.get("/manage/:guildID", checkAuth, (req, res) => {
const guild = client.guilds.get(req.params.guildID);
if (!guild) return res.status(404);
const isManaged =
guild && !!guild.member(req.user.id)
? guild.member(req.user.id).permissions.has("MANAGE_GUILD")
: false;
if (req.user.id === client.config.ownerID || req.user.id === client.config.subownerID) {
console.log(`Admin bypass for managing server: ${req.params.guildID}`);
} else if (!isManaged) {
res.redirect("/dashboard");
}
res.render(path.resolve(`${templateDir}${path.sep}manage.ejs`), {
bot: client,
guild: guild,
user: req.user,
auth: true
});
});
app.get("/leave/:guildID", checkAuth, async (req, res) => {
const guild = client.guilds.get(req.params.guildID);
if (!guild) return res.status(404);
const isManaged =
guild && !!guild.member(req.user.id)
? guild.member(req.user.id).permissions.has("MANAGE_GUILD")
: false;
if (req.user.id === client.config.ownerID || req.user.id === client.config.subownerID) {
console.log(`Admin bypass for managing server: ${req.params.guildID}`);
} else if (!isManaged) {
res.redirect("/dashboard");
}
await guild.leave();
if (req.user.id === client.config.ownerID || req.user.id === client.config.subownerID) {
return res.redirect("/admin");
}
res.redirect("/dashboard");
});
app.get("/reset/:guildID", checkAuth, async (req, res) => {
const guild = client.guilds.get(req.params.guildID);
if (!guild) return res.status(404);
const isManaged =
guild && !!guild.member(req.user.id)
? guild.member(req.user.id).permissions.has("MANAGE_GUILD")
: false;
if (req.user.id === client.config.ownerID || req.user.id === client.config.subownerID) {
console.log(`Admin bypass for managing server: ${req.params.guildID}`);
} else if (!isManaged) {
res.redirect("/dashboard");
}
client.settings.set(guild.id, client.config.defaultSettings);
res.redirect(`/manage/${req.params.guildID}`);
});
app.get("/commands", (req, res) => {
if (req.isAuthenticated()) {
res.render(path.resolve(`${templateDir}${path.sep}commands.ejs`), {
bot: client,
auth: true,
user: req.user,
md: md
});
} else {
res.render(path.resolve(`${templateDir}${path.sep}commands.ejs`), {
bot: client,
auth: false,
user: null,
md: md
});
}
});
app.get("/logout", function(req, res) {
req.logout();
res.redirect("/");
});
app.get("*", function(req, res) {
// Catch-all 404
res.send(
'<p>404 File Not Found. Please wait...<p> <script>setTimeout(function () { window.location = "/"; }, 1000);</script><noscript><meta http-equiv="refresh" content="1; url=/" /></noscript>'
);
});
/*app.get("/api/:action", async (req, res) => {
let apicodes = await db.fetch(`apicodes`)
console.log('1')
let action = req.params.action
if (apicodes) {
console.log('2')
if (apicodes.keys.includes(req.query.key)) {
if (apicodes.secrets.includes(req.query.secret)) {
if (action) {
if (action === 'check') {
let check = await db.fetch(`gban_${req.params.userid}`)
if (check) {
res.send(check)
} else {
res.send('Not Banned')
}
} else {
if (action === 'ban') {
let user = req.query.userid,
reason = req.query.reason,
evidence = req.query.evidence,
moderator = req.query.moderator,
requester = req.query.botid;
let request = {type: 'ban', userID: user, reason: reason, evidence: evidence, moderator: moderator, botid: requester, status: 'Not checked'}
await db.set(`banrequests_${requester}`)
} else {
if (action === 'unban') {
let user = req.query.userid,
reason = req.query.reason,
moderator = req.query.moderator,
requester = req.query.botid;
let request = {type: 'unban', userID: user, reason: reason, moderator: moderator, botid: requester, status: 'Not checked'}
await db.set(`banrequests_${requester}`)
} else {
if (action === 'editban') {
if (req.query.reason) {
let user = req.query.userid,
reason = req.query.reason,
moderator = req.query.moderator,
requester = req.query.botid;
let request = {type: 'editban', userID: user, reason: reason, moderator: moderator, botid: requester, status: 'Not checked'}
await db.set(`banrequests_${requester}`)
} else {
if (req.query.evidence) {
let user = req.query.userid,
evidence = req.query.evidence,
moderator = req.query.moderator,
requester = req.query.botid;
let request = {type: 'editban', userID: user, evidence: evidence, moderator: moderator, botid: requester, status: 'Not checked'}
await db.set(`banrequests_${requester}`)
}
}
} else {
if (action === 'requests') {
let requester = req.query.botid
let requests = await db.fetch(`banrequests_${requester}`)
if (requests) {
res.send(requests)
} else {
res.send('No requests.')
}
} else {
if (action === 'checkcon') {
res.status(200).send('OK.')
}
}
}
}
}
}
} else {
console.log('2')
res.send('Invalid API call method.')}//.status(401)
} else res.send('Invalid API auth secret.')//.status(401)
} else res.send('Invalid API auth key.')//.status(401)
} else res.send('API code check broken')//.status(401)
});*/
console.log('a')
const getAPICodes = async () => await db.fetch(`apicodes`);
const getBannedStatus = async id => await db.fetch(`gban_${id}`);
const banRequest = async (req, type) => {
const { userid: userId } = req.params;
const { reason, evidence, moderator, botid: requester } = req.query;
const request = {
type: type || "ban",
userID: userId,
reason: reason,
evidence: evidence,
moderator: moderator,
botid: requester,
status: "Not checked"
};
return await db.set(`banrequests_${requester}`, request);
};
const actionHandler = async (req, res) => {
console.log('1')
const apiCodes = await getAPICodes();
const { keys: apiCodesKeys, secrets: apiCodesSecrets } = apiCodes;
const { action, userid: userId } = req.params;
const { key, secret } = req.query;
const passingState =
apiCodesKeys.includes(key) && apiCodesSecrets.includes(secret);
if (!action || !apiCodes || !passingState) return res.status(401).json({
message: "Unauthorized",
code: 401
})
switch (action) {
case "check": {
const check = getBannedStatus(userId);
if (check) {
res.send(check);
} else {
res.send("Not Banned");
}
}
case "ban": {
res.json(banRequest(req, "ban"));
}
case "unban": {
res.json(banRequest(req, "unban"));
}
case "editban": {
res.json(banRequest(req, "editban"));
}
case "requests": {
res.json(await db.get(`banrequests_${req.paramd.botid}`));
}
case "checkon": {
res.sendStatus(200);
}
}
};
app.get("/api/:action", async (req, res) => await actionHandler(req, res));
client.site = app
.listen(client.config.dashboard.port, function() {
client.log(
"log",
`Dashboard running on port ${client.config.dashboard.port}`,
"INFO"
);
})
.on("error", err => {
client.log("ERROR", `Error with starting dashboard: ${err.code}`);
return process.exit(0);
});
};
I call the dashboard.js in the ready event with: require('../modules/dashboard')(client);