├── dashboard ├── templates │ ├── partials │ │ ├── footer.ejs │ │ └── header.ejs │ ├── index.ejs │ ├── settings.ejs │ └── dashboard.ejs └── dashboard.js ├── models └── settings.js ├── _config.js ├── README.md ├── package.json ├── LICENSE ├── .gitignore └── index.js /dashboard/templates/partials/footer.ejs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /dashboard/templates/index.ejs: -------------------------------------------------------------------------------- 1 | <%- include("partials/header", { bot, user, path, title: "Home" }) %> 2 | 3 | Go to Dashboard
4 | 5 | <%- include("partials/footer") %> -------------------------------------------------------------------------------- /models/settings.js: -------------------------------------------------------------------------------- 1 | // We grab Schema and model from mongoose library. 2 | const { Schema, model } = require("mongoose"); 3 | 4 | // We declare new schema. 5 | const guildSettingSchema = new Schema({ 6 | gid: { type: String }, 7 | prefix: { type: String, default: "!" } 8 | }); 9 | 10 | // We export it as a mongoose model. 11 | module.exports = model("guild_settings", guildSettingSchema); -------------------------------------------------------------------------------- /dashboard/templates/partials/header.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= title %> 6 | 7 | 8 | <% if (user) { %> 9 |

Logged in as <%= user.username %>#<%= user.discriminator %>.

10 | Logout 11 | <% } else { %> 12 | Login
13 | <% } %> 14 | <%- title === "Dashboard" ? "" : "
" %> -------------------------------------------------------------------------------- /dashboard/templates/settings.ejs: -------------------------------------------------------------------------------- 1 | <%- include("partials/header", { bot, user, path, title: "Home" }) %> 2 | 3 | <% if (alert) { %> 4 |

<%= alert %>


5 | <% } %> 6 | 7 |

<%= guild.name %>

8 |
9 |

Prefix:

10 |

11 | 12 |
13 | 14 | <%- include("partials/footer") %> -------------------------------------------------------------------------------- /_config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "token": "TOKEN", // https://discordapp.com/developers/applications/ID/bot 3 | "mongodbUrl": "MONGODB_URL", // Mongodb connection url. 4 | "id": "CLIENT_ID", // https://discordapp.com/developers/applications/ID/information 5 | "clientSecret": "CLIENT_SECRET", // https://discordapp.com/developers/applications/ID/information 6 | "domain": "http://localhost", 7 | "port": 8080 8 | }; 9 | 10 | /** 11 | * !!! 12 | * You need to add a redirect url to https://discordapp.com/developers/applications/ID/oauth2. 13 | * Format is: domain:port/callback example http://localhost:8080/callback 14 | * - Do not include port if the port is 80. 15 | * !!! 16 | */ -------------------------------------------------------------------------------- /dashboard/templates/dashboard.ejs: -------------------------------------------------------------------------------- 1 | <%- include("partials/header", { bot, user, path, title: "Dashboard" }) %> 2 | 3 | <% user.guilds.forEach(guild => { 4 | const permsOnGuild = new perms(guild.permissions); 5 | if(!permsOnGuild.has("MANAGE_GUILD")) return; 6 | %> 7 |
8 |

<%= guild.name %>

9 | <% if (bot.guilds.cache.get(guild.id)) { %> 10 | Edit Settings 11 | <% } else { %> 12 | ">Add Bot 13 | <% } %> 14 | <% }); %> 15 | <%- include("partials/footer") %> -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord Bot Dashboard 2 | Is a very simple, no-css bot dashboard. 3 | Made with Express, MongoDB, Discord.JS. 4 | 5 | ## Discord Server 6 | You can join my discord server [here](https://discord.gg/rk7cVyk). 7 | ## Features 8 | * Custom Prefix 9 | * Main Page 10 | * Server Selection Page 11 | * Settings Edit Page 12 | * Dynamically rendered with ejs templating engine 13 | * Ping Command 14 | 15 | ## Setup 16 | 1) Clone this repository locally. 17 | ``` 18 | git clone https://github.com/MrAugu/discord-bot-dashboard 19 | ``` 20 | 2) Rename `_config.js` to `config.js`. 21 | 4) Fill the required values. 22 | 3) Install all of the required modules. 23 | ``` 24 | npm install 25 | ``` 26 | 4) Add the callback url to the bot's OAuth page. (see more in config.js) 27 | 5) Start the dashboard. 28 | ``` 29 | node index 30 | ``` 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discord-bot-dashboard-tutorial", 3 | "version": "1.0.0", 4 | "description": "A simple discord bot dashboard which focuses on the dashboard itself.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node index.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/MrAugu/discord-bot-dashboard.git" 12 | }, 13 | "keywords": [ 14 | "discord.js", 15 | "bot", 16 | "dashboard" 17 | ], 18 | "author": "MrAugu#7917", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/MrAugu/discord-bot-dashboard/issues" 22 | }, 23 | "homepage": "https://github.com/MrAugu/discord-bot-dashboard#readme", 24 | "dependencies": { 25 | "body-parser": "^1.19.0", 26 | "discord.js": "^12.1.1", 27 | "ejs": "^3.0.2", 28 | "express": "^4.17.1", 29 | "express-session": "^1.17.0", 30 | "memorystore": "^1.6.2", 31 | "mongoose": "^5.9.9", 32 | "passport": "^0.4.1", 33 | "passport-discord": "^0.1.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 MrAugu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Config file 107 | config.js -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | > Index.Js is the entry point of our application. 3 | */ 4 | // We import the modules. 5 | const Discord = require("discord.js"); 6 | const mongoose = require("mongoose"); 7 | const config = require("./config"); 8 | const GuildSettings = require("./models/settings"); 9 | const Dashboard = require("./dashboard/dashboard"); 10 | 11 | // We instiate the client and connect to database. 12 | const client = new Discord.Client(); 13 | mongoose.connect(config.mongodbUrl, { 14 | useNewUrlParser: true, 15 | useUnifiedTopology: true 16 | }); 17 | client.config = config; 18 | 19 | // We listen for client's ready event. 20 | client.on("ready", () => { 21 | console.log(`Bot is ready. (${client.guilds.cache.size} Guilds - ${client.channels.cache.size} Channels - ${client.users.cache.size} Users)`); 22 | Dashboard(client); 23 | }); 24 | 25 | // We listen for message events. 26 | client.on("message", async (message) => { 27 | // Declaring a reply function for easier replies - we grab all arguments provided into the function and we pass them to message.channel.send function. 28 | const reply = (...arguments) => message.channel.send(...arguments); 29 | 30 | // Doing some basic command logic. 31 | if (message.author.bot) return; 32 | if (!message.channel.permissionsFor(message.guild.me).has("SEND_MESSAGES")) return; 33 | 34 | // Retriving the guild settings from database. 35 | var storedSettings = await GuildSettings.findOne({ gid: message.guild.id }); 36 | if (!storedSettings) { 37 | // If there are no settings stored for this guild, we create them and try to retrive them again. 38 | const newSettings = new GuildSettings({ 39 | gid: message.guild.id 40 | }); 41 | await newSettings.save().catch(()=>{}); 42 | storedSettings = await GuildSettings.findOne({ gid: message.guild.id }); 43 | } 44 | 45 | // If the message does not start with the prefix stored in database, we ignore the message. 46 | if (message.content.indexOf(storedSettings.prefix) !== 0) return; 47 | 48 | // We remove the prefix from the message and process the arguments. 49 | const args = message.content.slice(storedSettings.prefix.length).trim().split(/ +/g); 50 | const command = args.shift().toLowerCase(); 51 | 52 | // If command is ping we send a sample and then edit it with the latency. 53 | if (command === "ping") { 54 | const roundtripMessage = await reply("Pong!"); 55 | return roundtripMessage.edit(`*${roundtripMessage.createdTimestamp - message.createdTimestamp}ms*`); 56 | } 57 | }); 58 | 59 | // Listening for error & warn events. 60 | client.on("error", console.error); 61 | client.on("warn", console.warn); 62 | 63 | // We login into the bot. 64 | client.login(config.token); -------------------------------------------------------------------------------- /dashboard/dashboard.js: -------------------------------------------------------------------------------- 1 | // We import modules. 2 | const url = require("url"); 3 | const path = require("path"); 4 | const express = require("express"); 5 | const passport = require("passport"); 6 | const session = require("express-session"); 7 | const Strategy = require("passport-discord").Strategy; 8 | const config = require("../config"); 9 | const ejs = require("ejs"); 10 | const bodyParser = require("body-parser"); 11 | const Discord = require("discord.js"); 12 | const GuildSettings = require("../models/settings"); 13 | 14 | // We instantiate express app and the session store. 15 | const app = express(); 16 | const MemoryStore = require("memorystore")(session); 17 | 18 | // We export the dashboard as a function which we call in ready event. 19 | module.exports = async (client) => { 20 | // We declare absolute paths. 21 | const dataDir = path.resolve(`${process.cwd()}${path.sep}dashboard`); // The absolute path of current this directory. 22 | const templateDir = path.resolve(`${dataDir}${path.sep}templates`); // Absolute path of ./templates directory. 23 | 24 | // Deserializing and serializing users without any additional logic. 25 | passport.serializeUser((user, done) => done(null, user)); 26 | passport.deserializeUser((obj, done) => done(null, obj)); 27 | 28 | // We set the passport to use a new discord strategy, we pass in client id, secret, callback url and the scopes. 29 | /** Scopes: 30 | * - Identify: Avatar's url, username and discriminator. 31 | * - Guilds: A list of partial guilds. 32 | */ 33 | passport.use(new Strategy({ 34 | clientID: config.id, 35 | clientSecret: config.clientSecret, 36 | callbackURL: `${config.domain}${config.port == 80 ? "" : `:${config.port}`}/callback`, 37 | scope: ["identify", "guilds"] 38 | }, 39 | (accessToken, refreshToken, profile, done) => { // eslint-disable-line no-unused-vars 40 | // On login we pass in profile with no logic. 41 | process.nextTick(() => done(null, profile)); 42 | })); 43 | 44 | // We initialize the memorystore middleware with our express app. 45 | app.use(session({ 46 | store: new MemoryStore({ checkPeriod: 86400000 }), 47 | secret: "#@%#&^$^$%@$^$&%#$%@#$%$^%&$%^#$%@#$%#E%#%@$FEErfgr3g#%GT%536c53cc6%5%tv%4y4hrgrggrgrgf4n", 48 | resave: false, 49 | saveUninitialized: false, 50 | })); 51 | 52 | // We initialize passport middleware. 53 | app.use(passport.initialize()); 54 | app.use(passport.session()); 55 | 56 | // We bind the domain. 57 | app.locals.domain = config.domain.split("//")[1]; 58 | 59 | // We set out templating engine. 60 | app.engine("html", ejs.renderFile); 61 | app.set("view engine", "html"); 62 | 63 | // We initialize body-parser middleware to be able to read forms. 64 | app.use(bodyParser.json()); 65 | app.use(bodyParser.urlencoded({ 66 | extended: true 67 | })); 68 | 69 | // We declare a renderTemplate function to make rendering of a template in a route as easy as possible. 70 | const renderTemplate = (res, req, template, data = {}) => { 71 | // Default base data which passed to the ejs template by default. 72 | const baseData = { 73 | bot: client, 74 | path: req.path, 75 | user: req.isAuthenticated() ? req.user : null 76 | }; 77 | // We render template using the absolute path of the template and the merged default data with the additional data provided. 78 | res.render(path.resolve(`${templateDir}${path.sep}${template}`), Object.assign(baseData, data)); 79 | }; 80 | 81 | // We declare a checkAuth function middleware to check if an user is logged in or not, and if not redirect him. 82 | const checkAuth = (req, res, next) => { 83 | // If authenticated we forward the request further in the route. 84 | if (req.isAuthenticated()) return next(); 85 | // If not authenticated, we set the url the user is redirected to into the memory. 86 | req.session.backURL = req.url; 87 | // We redirect user to login endpoint/route. 88 | res.redirect("/login"); 89 | } 90 | 91 | // Login endpoint. 92 | app.get("/login", (req, res, next) => { 93 | // We determine the returning url. 94 | if (req.session.backURL) { 95 | req.session.backURL = req.session.backURL; // eslint-disable-line no-self-assign 96 | } else if (req.headers.referer) { 97 | const parsed = url.parse(req.headers.referer); 98 | if (parsed.hostname === app.locals.domain) { 99 | req.session.backURL = parsed.path; 100 | } 101 | } else { 102 | req.session.backURL = "/"; 103 | } 104 | // Forward the request to the passport middleware. 105 | next(); 106 | }, 107 | passport.authenticate("discord")); 108 | 109 | // Callback endpoint. 110 | app.get("/callback", passport.authenticate("discord", { failureRedirect: "/" }), /* We authenticate the user, if user canceled we redirect him to index. */ (req, res) => { 111 | // If user had set a returning url, we redirect him there, otherwise we redirect him to index. 112 | if (req.session.backURL) { 113 | const url = req.session.backURL; 114 | req.session.backURL = null; 115 | res.redirect(url); 116 | } else { 117 | res.redirect("/"); 118 | } 119 | }); 120 | 121 | // Logout endpoint. 122 | app.get("/logout", function (req, res) { 123 | // We destroy the session. 124 | req.session.destroy(() => { 125 | // We logout the user. 126 | req.logout(); 127 | // We redirect user to index. 128 | res.redirect("/"); 129 | }); 130 | }); 131 | 132 | // Index endpoint. 133 | app.get("/", (req, res) => { 134 | renderTemplate(res, req, "index.ejs"); 135 | }); 136 | 137 | // Dashboard endpoint. 138 | app.get("/dashboard", checkAuth, (req, res) => { 139 | renderTemplate(res, req, "dashboard.ejs", { perms: Discord.Permissions }); 140 | }); 141 | 142 | // Settings endpoint. 143 | app.get("/dashboard/:guildID", checkAuth, async (req, res) => { 144 | // We validate the request, check if guild exists, member is in guild and if member has minimum permissions, if not, we redirect it back. 145 | const guild = client.guilds.cache.get(req.params.guildID); 146 | if (!guild) return res.redirect("/dashboard"); 147 | const member = guild.members.cache.get(req.user.id); 148 | if (!member) return res.redirect("/dashboard"); 149 | if (!member.permissions.has("MANAGE_GUILD")) return res.redirect("/dashboard"); 150 | 151 | // We retrive the settings stored for this guild. 152 | var storedSettings = await GuildSettings.findOne({ gid: guild.id }); 153 | if (!storedSettings) { 154 | // If there are no settings stored for this guild, we create them and try to retrive them again. 155 | const newSettings = new GuildSettings({ 156 | gid: guild.id 157 | }); 158 | await newSettings.save().catch(()=>{}); 159 | storedSettings = await GuildSettings.findOne({ gid: guild.id }); 160 | } 161 | 162 | renderTemplate(res, req, "settings.ejs", { guild, settings: storedSettings, alert: null }); 163 | }); 164 | 165 | // Settings endpoint. 166 | app.post("/dashboard/:guildID", checkAuth, async (req, res) => { 167 | // We validate the request, check if guild exists, member is in guild and if member has minimum permissions, if not, we redirect it back. 168 | const guild = client.guilds.cache.get(req.params.guildID); 169 | if (!guild) return res.redirect("/dashboard"); 170 | const member = guild.members.cache.get(req.user.id); 171 | if (!member) return res.redirect("/dashboard"); 172 | if (!member.permissions.has("MANAGE_GUILD")) return res.redirect("/dashboard"); 173 | // We retrive the settings stored for this guild. 174 | var storedSettings = await GuildSettings.findOne({ gid: guild.id }); 175 | if (!storedSettings) { 176 | // If there are no settings stored for this guild, we create them and try to retrive them again. 177 | const newSettings = new GuildSettings({ 178 | gid: guild.id 179 | }); 180 | await newSettings.save().catch(()=>{}); 181 | storedSettings = await GuildSettings.findOne({ gid: guild.id }); 182 | } 183 | 184 | // We set the prefix of the server settings to the one that was sent in request from the form. 185 | storedSettings.prefix = req.body.prefix; 186 | // We save the settings. 187 | await storedSettings.save().catch(() => {}); 188 | 189 | // We render the template with an alert text which confirms that settings have been saved. 190 | renderTemplate(res, req, "settings.ejs", { guild, settings: storedSettings, alert: "Your settings have been saved." }); 191 | }); 192 | 193 | app.listen(config.port, null, null, () => console.log(`Dashboard is up and running on port ${config.port}.`)); 194 | }; --------------------------------------------------------------------------------