├── .github └── dependabot.yml ├── .gitignore ├── Caddyfile ├── LICENSE ├── README.md ├── backend ├── database.js ├── models │ ├── DiscordUser.js │ ├── OAuth2Credentials.js │ ├── customresponses.js │ ├── levelconfig.js │ ├── levels.js │ ├── prefix.js │ ├── userpetitions.js │ └── welcome.js ├── package.json ├── routes │ ├── auth.js │ ├── guilds.js │ └── main.js ├── server.js ├── strategies │ └── discord.js └── utils │ ├── permissions.json │ └── utils.js ├── frontend ├── .browserslistrc ├── .eslintrc.js ├── package.json ├── public │ ├── avatar.jpg │ ├── favicon.ico │ ├── index.html │ └── robots.txt ├── src │ ├── App.vue │ ├── main.js │ ├── partials │ │ ├── comment.vue │ │ ├── footer.vue │ │ ├── guild.vue │ │ └── nav.vue │ ├── router.js │ └── sections │ │ ├── 404.vue │ │ ├── dashboard.vue │ │ ├── dashboard │ │ ├── cp.vue │ │ ├── default.vue │ │ ├── levelconfig.vue │ │ ├── prefix.vue │ │ └── welcome.vue │ │ ├── feedback.vue │ │ ├── guilds.vue │ │ ├── main.vue │ │ ├── pending_comments.vue │ │ ├── pending_comments_delete.vue │ │ ├── stats.vue │ │ └── thanks.vue └── vue.config.js └── jsconfig.json /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/frontend" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | versioning-strategy: increase 9 | - package-ecosystem: npm 10 | directory: "/backend" 11 | schedule: 12 | interval: daily 13 | open-pull-requests-limit: 10 14 | assignees: 15 | - AndreMor8 16 | labels: 17 | - dependencies 18 | versioning-strategy: increase 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | backend/node_modules/ 2 | frontend/node_modules/ 3 | /public/ 4 | backend/.env 5 | backend/package-lock.json 6 | frontend/package-lock.json -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | # Replace paths and ports depending on your environment 2 | gidget.andremor.dev { 3 | encode gzip 4 | @back-end path /api/* 5 | handle @back-end { 6 | reverse_proxy 127.0.0.1:61531 7 | } 8 | 9 | handle { 10 | root * /home/andre/dashboard/public 11 | try_files {path} /index.html 12 | file_server 13 | } 14 | tls /home/andre/certs/cert.crt /home/andre/certs/cert.key 15 | } 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 AndreMor 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gidget's dashboard 2 | 3 | Rewriting my bot's dashboard, separating the backend into a `/api` path, and using Vue.js for the front-end. 4 | 5 | Well now I explain the thing 6 | 7 | ## Install vue-cli 8 | 9 | `npm i -g @vue/cli` 10 | 11 | ## Build the app 12 | 13 | Go to the frontend folder. 14 | 15 | Install front-end dependencies with `npm i` 16 | 17 | Run `npm run build` or `vue-cli-service build`. 18 | 19 | ## Install the backend dependencies 20 | 21 | `npm i` 22 | 23 | ## Configure the backend .env with the correct data. 24 | 25 | Soon an example of it. 26 | 27 | ## Run the backend 28 | 29 | `npm start` 30 | 31 | If you chose `SERVESTATIC='true'`, then just point to the correct port on your favorite web server. 32 | 33 | Otherwise configure your server to always serve `index.html` from the public folder, and the paths with `/api` to the backend. 34 | 35 | See `Caddyfile` for an example on the [Caddy](https://caddyserver.com/) web server. -------------------------------------------------------------------------------- /backend/database.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | mongoose.set('useFindAndModify', false); 3 | module.exports = async function() { 4 | await mongoose.connect(`mongodb+srv://${process.env.MDB_PATH}`, { useNewUrlParser: true, useUnifiedTopology: true, useFindAndModify: false, user: process.env.MDB_USER, pass: process.env.MDB_PASS, dbName: process.env.MDB_DBNAME }); 5 | console.log("Connected to the database"); 6 | } -------------------------------------------------------------------------------- /backend/models/DiscordUser.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const UserSchema = new mongoose.Schema({ 4 | discordId: { type: String, required: true }, 5 | username: { type: String, required: true }, 6 | avatar: { type: String }, 7 | premium_type: { type: Number } 8 | }); 9 | 10 | const DiscordUser = module.exports = mongoose.model('User', UserSchema); -------------------------------------------------------------------------------- /backend/models/OAuth2Credentials.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const schema = new mongoose.Schema({ 4 | accessToken: { type: String, required: true }, 5 | refreshToken: { type: String, required: true }, 6 | discordId: { type: String, required: true } 7 | }) 8 | 9 | module.exports = mongoose.model("OAuth2Credential", schema); -------------------------------------------------------------------------------- /backend/models/customresponses.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const MessageSchema = new mongoose.Schema({ 3 | guildId: { type: String, required: true }, 4 | responses: { type: mongoose.Schema.Types.Mixed, default: {} } 5 | }); 6 | 7 | const MessageModel = module.exports = mongoose.model('customresponse', MessageSchema); -------------------------------------------------------------------------------- /backend/models/levelconfig.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const MessageSchema = new mongoose.Schema({ 4 | guildId: { type: String, required: true }, 5 | levelnotif: { type: Boolean, default: false }, 6 | levelsystem: { type: Boolean, default: false }, 7 | roles: { type: Array, default: [] } 8 | }); 9 | 10 | const MessageModel = module.exports = mongoose.model('levelconfig', MessageSchema); -------------------------------------------------------------------------------- /backend/models/levels.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const LevelSchema = new mongoose.Schema({ 4 | userID: { type: String }, 5 | guildID: { type: String }, 6 | xp: { type: Number, default: 0 }, 7 | level: { type: Number, default: 0 }, 8 | lastUpdated: { type: Date, default: new Date() } 9 | }); 10 | 11 | const Levels = module.exports = mongoose.model('Levels', LevelSchema); -------------------------------------------------------------------------------- /backend/models/prefix.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const MessageSchema = new mongoose.Schema({ 3 | guildId: { type: String, required: true }, 4 | prefix: { type: String, required: true, default: "g%" } 5 | }); 6 | 7 | const MessageModel = module.exports = mongoose.model('prefix', MessageSchema); -------------------------------------------------------------------------------- /backend/models/userpetitions.js: -------------------------------------------------------------------------------- 1 | //0 = suggestion, 1 = bug/report 2 | const mongoose = require("mongoose"); 3 | const sch = new mongoose.Schema({ 4 | userID: { type: String, required: true }, 5 | type: { type: Number, default: 0 }, 6 | text: { type: String, required: true }, 7 | anon: { type: Boolean, default: false } 8 | }); 9 | module.exports = mongoose.model("petition", sch); -------------------------------------------------------------------------------- /backend/models/welcome.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const chema = new mongoose.Schema({ 4 | guildID: { type: String, required: true }, 5 | enabled: { type: Boolean, default: false }, 6 | text: { type: String, default: "Welcome to the server, %MEMBER%" }, 7 | channelID: { type: String, default: null }, 8 | dmenabled: { type: Boolean, default: false }, 9 | dmtext: { type: String, default: "Welcome to the server, %MEMBER%" }, 10 | leaveenabled: { type: Boolean, default: false }, 11 | leavechannelID: { type: String, default: null }, 12 | leavetext: { type: String, default: "We're sorry to see you leaving, %MEMBER%" } 13 | }); 14 | 15 | module.exports = mongoose.model("welcome", chema); -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gidgetdashboard-backend", 3 | "version": "2.0.0", 4 | "description": "", 5 | "main": "./server.js", 6 | "scripts": { 7 | "start": "node ." 8 | }, 9 | "engines": { 10 | "node": ">=15.6.0" 11 | }, 12 | "author": "AndreMor#0008", 13 | "license": "Apache-2.0", 14 | "dependencies": { 15 | "connect-mongo": "^4.4.1", 16 | "crypto-js": "^4.0.0", 17 | "csurf": "^1.11.0", 18 | "discord.js": "^12.5.3", 19 | "dotenv": "^16.0.3", 20 | "express": "^4.17.1", 21 | "express-session": "^1.17.2", 22 | "moment": "^2.29.1", 23 | "moment-duration-format": "^2.3.2", 24 | "mongoose": "^5.13.15", 25 | "node-fetch": "^2.6.7", 26 | "passport": "^0.6.0", 27 | "passport-discord": "^0.1.4", 28 | "safe-regex": "^2.1.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/routes/auth.js: -------------------------------------------------------------------------------- 1 | const keys = new Set(); 2 | const router = require('express').Router(); 3 | const passport = require('passport'); 4 | 5 | router.get('/', isAuthorized, (req, res, next) => { 6 | const key = req.csrfToken(); 7 | if(req.query.error) { 8 | keys.delete(req.query.state); 9 | return res.redirect(302, "/"); 10 | } else if(req.query.code) { 11 | if(typeof req.query.state !== "string") return res.status(400).send("No state query found. Try re-login!"); 12 | if(!keys.has(req.query.state)) return res.status(400).send("Invalid state key! Try re-login!"); 13 | keys.delete(req.query.state); 14 | } else { 15 | keys.add(key); 16 | } 17 | passport.authenticate('discord', { 18 | state: key, 19 | failureMessage: true, 20 | scope: ["identify", "guilds"], 21 | successRedirect: "https://gidget.andremor.dev", 22 | callbackURL: process.env.CLIENT_REDIRECT 23 | })(req, res, next); 24 | }); 25 | 26 | router.get('/logout', (req, res) => { 27 | if (req.user) { 28 | req.logout((err) => { 29 | if(err) res.status(500).send(err.toString()); 30 | else res.redirect('/'); 31 | }); 32 | } else res.redirect('/'); 33 | }); 34 | 35 | function isAuthorized(req, res, next) { 36 | if (req.user) { 37 | res.redirect('/') 38 | } 39 | else { 40 | next(); 41 | } 42 | } 43 | 44 | module.exports = router; 45 | -------------------------------------------------------------------------------- /backend/routes/guilds.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | 3 | const cp = require("../models/customresponses.js"); 4 | const prefix = require("../models/prefix.js"); 5 | const levels = require("../models/levelconfig.js"); 6 | const welcome = require("../models/welcome.js"); 7 | const util = require("../utils/utils"); 8 | const fetch = require("node-fetch"); 9 | const safe = require("safe-regex"); 10 | 11 | router.use(async function (req, res, next) { 12 | try { 13 | const guilds = await util.getUserGuilds(req.user.discordId); 14 | req.user.guilds = guilds; 15 | const bot_thing = await util.getBotGuilds(guilds); 16 | const toshow = util.getGuilds(bot_thing, guilds); 17 | req.user.toShowGuilds = toshow; 18 | next(); 19 | } catch (err) { 20 | console.log(err); 21 | res.status(500).redirect("/"); 22 | } 23 | }) 24 | 25 | 26 | router.get("/", async (req, res) => { 27 | const adminguilds = req.user.guilds.filter(e => util.getPermissions(e.permissions).get("ADMINISTRATOR")); 28 | res.status(200).json({ 29 | userID: req.user.discordId, 30 | guilds: req.user.toShowGuilds, 31 | adminguilds 32 | }); 33 | }); 34 | 35 | router.use("/:guildID", function (req, res, next) { 36 | const guild = req.user.toShowGuilds.find(e => e.id === req.params.guildID); 37 | if (!guild) return res.status(403).send("That ID is not in your server list..."); 38 | else { 39 | if (!["GET", "HEAD", "OPTIONS"].includes(req.method)) { 40 | fetch(process.env.FETCH + "?delete=" + req.params.guildID, { 41 | method: "GET", 42 | headers: { 43 | pass: process.env.ACCESS 44 | } 45 | }).then(c => console.log(c.status)).catch(err => { }); 46 | } 47 | next(); 48 | } 49 | }); 50 | 51 | router.get("/:guildID", async (req, res) => { 52 | const guild = req.user.toShowGuilds.find(e => e.id === req.params.guildID); 53 | res.status(200).json({ 54 | userID: req.user.discordId, 55 | name: guild.name, 56 | id: guild.id 57 | }); 58 | }); 59 | /* 60 | router.get("/:guildID/members", async (req, res) => { 61 | const members = await util.getGuildMembers(req.params.guildID, 100); 62 | console.log(members); 63 | res.status(200).render("dashboard1", { 64 | username: req.user.username, 65 | csrfToken: req.csrfToken(), 66 | avatar: req.user.avatar, 67 | discordId: req.user.discordId, 68 | guilds: req.user.guilds, 69 | toshow: req.user.toShowGuilds, 70 | focus: req.params.guildID, 71 | logged: true, 72 | option: "members", 73 | data: members, 74 | avatar: function avatar([a, id], d) { 75 | if (a) { 76 | if (a.startsWith("a_")) return `https://cdn.discordapp.com/avatars/${id}/${a}.gif?size=128`; 77 | else return `https://cdn.discordapp.com/avatars/${id}/${a}.png?size=128`; 78 | } else { 79 | return `https://cdn.discordapp.com/embed/avatars/${d % 5}.png`; 80 | } 81 | } 82 | }); 83 | }); */ 84 | 85 | router.get("/:guildID/levels", async (req, res) => { 86 | let msgDocument = await levels.findOne({ guildId: { $eq: req.params.guildID } }); 87 | if (!msgDocument) { 88 | msgDocument = await levels.create({ 89 | guildId: req.params.guildID, 90 | levelnotif: false, 91 | levelsystem: false, 92 | roles: [] 93 | }); 94 | } 95 | /* Render the data to front-end */ 96 | res.status(200).json({ 97 | userID: req.user.discordId, 98 | data: msgDocument 99 | }); 100 | }) 101 | 102 | router.put("/:guildID/levels", async (req, res) => { 103 | try { 104 | if (typeof req.body.system !== "boolean") return res.status(400).send("Missing or invalid system parameter"); 105 | if (typeof req.body.notif !== "boolean") return res.status(400).send("Missing or invalid notif parameter"); 106 | let msgDocument = await levels.findOne({ guildId: { $eq: req.params.guildID } }); 107 | if (msgDocument) { 108 | await msgDocument.updateOne({ levelsystem: req.body.system, levelnotif: req.body.notif }) 109 | } else { 110 | await levels.create({ 111 | levelsystem: req.body.system, 112 | levelnotif: req.body.notif, 113 | guildId: req.params.guildID, 114 | roles: [] 115 | }); 116 | } 117 | res.sendStatus(200); 118 | } catch (err) { 119 | res.status(500).send(err.toString()); 120 | } 121 | }); 122 | 123 | router.get("/:guildID/cp", async (req, res) => { 124 | let msgDocument = await cp.findOne({ guildId: { $eq: req.params.guildID } }); 125 | if (!msgDocument) { 126 | msgDocument = { 127 | guildId: req.params.guildID, 128 | responses: {} 129 | } 130 | } 131 | res.status(200).json({ 132 | userID: req.user.discordId, 133 | data: msgDocument 134 | }); 135 | }); 136 | 137 | router.post("/:guildID/cp", async (req, res) => { 138 | if (!req.body) return res.status(400).send("You haven't sent anything. Submit something and try again."); 139 | if (!req.body.match) return res.status(400).send("Missing match parameter"); 140 | if (!req.body.response) return res.status(400).send("Missing response parameter"); 141 | if (!safe(req.body.match)) return res.status(400).send("Invalid or insecure match parameter"); 142 | if (req.body.link) { 143 | if (!util.urlRegex.test(req.body.link)) { 144 | return res.status(400).send("Invalid file URL!"); 145 | } 146 | } 147 | let msgDocument = await cp.findOne({ guildId: { $eq: req.params.guildID } }); 148 | if (msgDocument) { 149 | let { responses } = msgDocument; 150 | const check = Object.keys(responses).find(e => e === req.body.match); 151 | if (check) return res.status(400).send("There is already a match of the same text. Put another.") 152 | const files = []; 153 | if (req.body.link) { 154 | files.push(req.body.link); 155 | } 156 | responses[req.body.match] = { content: req.body.response, files: files }; 157 | await msgDocument.updateOne({ responses: responses }); 158 | } else { 159 | const responses = {}; 160 | const files = []; 161 | if (req.body.link) { 162 | files.push(req.body.link); 163 | } 164 | responses[req.body.match] = { content: req.body.response, files: files }; 165 | await cp.create({ 166 | guildId: req.params.guildID, 167 | responses: responses 168 | }); 169 | } 170 | res.sendStatus(200); 171 | }); 172 | 173 | router.delete("/:guildID/cp", async (req, res) => { 174 | if (!req.body) return res.status(400).send("Nothing send"); 175 | if (isNaN(req.body.id)) return res.status(400).send("Missing or invalid ID parameter") 176 | let value = req.body.id; 177 | let msgDocument = await cp.findOne({ guildId: { $eq: req.params.guildID } }); 178 | if (msgDocument) { 179 | let { responses } = msgDocument 180 | const id = parseInt(value); 181 | const keys = Object.keys(responses); 182 | if (id < keys.length && id >= 0) { 183 | let word = keys[id]; 184 | if (responses.hasOwnProperty(word)) { 185 | delete responses[word]; 186 | const a = Object.keys(responses); 187 | if (a.length < 1) { 188 | await msgDocument.deleteOne() 189 | } else { 190 | await msgDocument.updateOne({ responses: responses }) 191 | } 192 | } 193 | } else res.sendStatus(400); 194 | } else { 195 | return res.sendStatus(404); 196 | } 197 | res.sendStatus(204); 198 | }) 199 | 200 | router.get("/:guildID/prefix", async (req, res) => { 201 | let msgDocument = await prefix.findOne({ guildId: req.params.guildID }); 202 | if (!msgDocument) msgDocument = await prefix.create({ 203 | guildId: req.params.guildID, 204 | prefix: "g%" 205 | }); 206 | res.status(200).json({ 207 | userID: req.user.discordId, 208 | data: msgDocument 209 | }); 210 | }); 211 | 212 | router.put("/:guildID/prefix", async (req, res) => { 213 | try { 214 | if (!req.body) return res.status(400).send("You haven't sent anything. Submit something and try again."); 215 | if (!req.body.prefix) return res.status(400).send("Missing parameter prefix"); 216 | let msgDocument = await prefix.findOne({ guildId: { $eq: req.params.guildID } }) 217 | if (msgDocument) { 218 | await msgDocument.updateOne({ prefix: req.body.prefix }); 219 | } else { 220 | await prefix.create({ 221 | guildId: req.params.guildID, 222 | prefix: req.body.prefix 223 | }); 224 | } 225 | res.sendStatus(200); 226 | } catch (err) { 227 | res.status(500).send(err.toString()); 228 | } 229 | }); 230 | 231 | router.get("/:guildID/welcome", async (req, res) => { 232 | let msgDocument = await welcome.findOne({ guildID: { $eq: req.params.guildID } }); 233 | if (!msgDocument) { 234 | msgDocument = { 235 | guildID: req.params.guildID, 236 | enabled: false, 237 | text: "Welcome to the server, %MEMBER%", 238 | channelID: null, 239 | dmenabled: false, 240 | dmtext: "Welcome to the server, %MEMBER%", 241 | leaveenabled: false, 242 | leavechannelID: null, 243 | leavetext: "We're sorry to see you leaving, %MEMBER%" 244 | } 245 | } 246 | const channels = await util.getGuildChannels(req.params.guildID); 247 | if (!Array.isArray(channels)) { 248 | console.error(channels); 249 | return res.status(500).send("Something happened! " + channels.message); 250 | } 251 | const textChannels = channels.filter(e => [0, 5].includes(e.type)); 252 | res.status(200).json({ 253 | userID: req.user.discordId, 254 | data: { 255 | document: msgDocument, 256 | textChannels, 257 | } 258 | }); 259 | }); 260 | 261 | router.put("/:guildID/welcome", async (req, res) => { 262 | if (typeof req.body.enabled !== "boolean") return res.sendStatus(400); 263 | if (!req.body.text) return res.sendStatus(400); 264 | if (!req.body.channelID) return res.sendStatus(400); 265 | if (typeof req.body.dmenabled !== "boolean") return res.sendStatus(400); 266 | if (!req.body.dmtext) return res.sendStatus(400); 267 | if (typeof req.body.leaveenabled !== "boolean") return res.sendStatus(400); 268 | if (!req.body.leavetext) return res.sendStatus(400); 269 | if (!req.body.leavechannelID) return res.sendStatus(400); 270 | try { 271 | const channels = await util.getGuildChannels(req.params.guildID); 272 | if (!Array.isArray(channels)) { 273 | console.error(channels); 274 | return res.status(500).send("Something happened! " + channels.message); 275 | } 276 | const textChannels = channels.filter(e => [0, 5].includes(e.type)); 277 | if (!textChannels.find(e => e.id === req.body.channelID)) return res.status(400).send("Invalid channel in 'channelID' parameter"); 278 | if (!textChannels.find(e => e.id === req.body.leavechannelID)) return res.status(400).send("Invalid channel in 'leavechannelID' parameter"); 279 | const msgDocument = await welcome.findOne({ guildID: { $eq: req.params.guildID } }); 280 | if (msgDocument) { 281 | await msgDocument.updateOne({ 282 | enabled: req.body.enabled, 283 | text: req.body.text, 284 | channelID: req.body.channelID, 285 | dmenabled: req.body.dmenabled, 286 | dmtext: req.body.dmtext, 287 | leaveenabled: req.body.leaveenabled, 288 | leavetext: req.body.leavetext, 289 | leavechannelID: req.body.leavechannelID 290 | }); 291 | } else { 292 | await welcome.create({ 293 | enabled: req.body.enabled, 294 | text: req.body.text, 295 | channelID: req.body.channelID, 296 | dmenabled: req.body.dmenabled, 297 | dmtext: req.body.dmtext, 298 | leaveenabled: req.body.leaveenabled, 299 | leavetext: req.body.leavetext, 300 | leavechannelID: req.body.leavechannelID 301 | }); 302 | } 303 | res.sendStatus(200); 304 | } catch (err) { 305 | res.status(500).send("Something happened! " + err.toString()); 306 | } 307 | }); 308 | 309 | module.exports = router; -------------------------------------------------------------------------------- /backend/routes/main.js: -------------------------------------------------------------------------------- 1 | const DiscordUser = require("../models/DiscordUser") 2 | const userpetitions = require("../models/userpetitions"); 3 | const router = require('express').Router(); 4 | const utils = require("../utils/utils"); 5 | const { MessageEmbed } = require("discord.js"); 6 | const cosas = ["577000793094488085"]; 7 | const os = require("os"); 8 | const moment = require("moment"); 9 | require("moment-duration-format"); 10 | 11 | router.get("/user", (req, res) => { 12 | res.cookie('XSRF-TOKEN', req.csrfToken()); 13 | if (req.user) { 14 | res.json({ 15 | hello: "world", 16 | loggedAs: { 17 | userID: req.user.discordId, 18 | username: req.user.username, 19 | avatar: req.user.avatar, 20 | } 21 | }); 22 | } else { 23 | res.json({ 24 | hello: "world" 25 | }); 26 | } 27 | }); 28 | 29 | router.post("/feedback", isAuthorized, async (req, res) => { 30 | if (![0, 1, 2, "0", "1", "2"].includes(req.body.type)) return res.sendStatus(400); 31 | if (!req.body.text) return res.sendStatus(400); 32 | if (req.body.text.length > 1950) return res.statusCode(400); 33 | if (typeof req.body.anon !== "boolean") return res.sendStatus(400); 34 | const text = req.body.text; 35 | const anon = req.body.anon; 36 | const type = Number(req.body.type); 37 | let typetext; 38 | if (type === 0) typetext = "Suggestion"; 39 | if (type === 1) typetext = "Normal feedback"; 40 | if (type === 2) typetext = "Report"; 41 | let color; 42 | if (type === 0) color = "GREEN"; 43 | if (type === 1) color = "BLUE"; 44 | if (type === 2) color = "RED"; 45 | let algo; 46 | if (type !== 1) algo = await userpetitions.create({ userID: req.user.discordId, type, text, anon }); 47 | const embed = new MessageEmbed() 48 | .setTitle("A new comment has been posted!") 49 | .setDescription(text) 50 | .setColor(color) 51 | if (anon) embed.setAuthor("Anonymous") 52 | else embed.setAuthor(req.user.username, utils.getAvatar(req.user)) 53 | embed.addField("Type", typetext) 54 | .addField("ID", (algo ? algo.id : "*It's a normal comment, so no need to save to DB*")) 55 | const m = await utils.createMessage("767287563735138335", { embed }); 56 | res.sendStatus(200); 57 | }); 58 | 59 | router.get("/pending-comments", async (req, res) => { 60 | const comments = await userpetitions.find(); 61 | const admin = req.user ? cosas.includes(req.user.discordId) : false; 62 | const tosend = [] 63 | for (let i in comments) { 64 | if (!comments[i].userID) continue; 65 | const user = await DiscordUser.findOne({ discordId: comments[i].userID }); 66 | if (user && (admin || !comments[i].anon)) { 67 | tosend.push({ 68 | _id: comments[i]._id, 69 | type: comments[i].type, 70 | userID: comments[i].userID, 71 | text: comments[i].text, 72 | anon: comments[i].anon, 73 | user: { username: user.username, avatar: utils.getAvatar(user), discordId: user.discordId } 74 | }); 75 | } else { 76 | tosend.push(comments[i]); 77 | } 78 | } 79 | res.status(200).json({ 80 | userID: req.user ? req.user.discordId : null, 81 | comments: tosend, 82 | admin 83 | }); 84 | }); 85 | 86 | router.delete("/pending-comments", isAuthorized, async (req, res) => { 87 | if (!cosas.includes(req.user.discordId)) return res.status(403).send("You must be a Gidget's bot developer to do this"); 88 | if (!req.body.id) return res.sendStatus(400); 89 | if (typeof req.body.d !== "boolean") return res.sendStatus(400); 90 | const doc = await userpetitions.findById(req.body.id); 91 | if (doc && [0, 2].includes(doc.type)) { 92 | const d = req.body.d; 93 | let text = req.body.message; 94 | if (d && !req.body.message) return res.sendStatus(400); 95 | if (req.body.message) { 96 | if (doc.type === 2) text = `Your report ID ${doc._id} was ${d ? "approved" : "rejected"}: ${req.body.message}`; 97 | else if (doc.type === 0) text = `Your suggestion ID ${doc._id} was ${d ? "approved" : "rejected"}: ${req.body.message}`; 98 | //Message DM 99 | const dm = await utils.createDM(doc.userID); 100 | await utils.createMessage(dm.id, { content: text }); 101 | await utils.createMessage("767287563735138335", { embed: new MessageEmbed().setTitle(`${d ? "Approved" : "Rejected"} comment`).addField("ID", doc._id).addField("Reason", req.body.message) }) 102 | } 103 | await doc.deleteOne(); 104 | res.sendStatus(200); 105 | } else res.sendStatus(404); 106 | }) 107 | 108 | router.get("/andremor", async (req, res) => { 109 | res.setHeader("Access-Control-Allow-Origin", "*"); 110 | const response = await utils.getUser("577000793094488085"); 111 | if (!response.message) return res.status(200).send(`${response.username}#${response.discriminator}`); 112 | else return res.status(500).send("AndreMor"); 113 | }); 114 | 115 | 116 | router.get("/stats", (req, res) => { 117 | res.setHeader("Access-Control-Allow-Origin", "*") 118 | const memoryrssusage = process.memoryUsage.rss(); 119 | const uptime = moment.duration((process.uptime() * 1000)).format("d [days], h [hours], m [minutes]"); 120 | const freemem = os.freemem(); 121 | const totalmem = os.totalmem(); 122 | const nodeversion = process.version; 123 | const hoster = process.env.HOSTER; 124 | const system = `${os.version()}\n${os.release()}`; 125 | const cpu = os.cpus()[0].model; 126 | const arch = os.arch(); 127 | const platform = os.platform(); 128 | res.json({ memoryrssusage, uptime, freemem, totalmem, nodeversion, hoster, system, cpu, arch, platform }); 129 | }) 130 | 131 | router.use("/auth", require("./auth")); 132 | router.use("/guilds", isAuthorized, require("./guilds")); 133 | 134 | function isAuthorized(req, res, next) { 135 | if (req.user) { 136 | next(); 137 | } 138 | else { 139 | res.redirect("/"); 140 | } 141 | } 142 | 143 | module.exports = router; -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config({ path: __dirname + "/.env" }); 2 | const db = require("./database.js"); 3 | const express = require("express"); 4 | const passport = require("passport"); 5 | const csrf = require('csurf'); 6 | const path = require('path'); 7 | const MongoStore = require('connect-mongo'); 8 | (async () => { 9 | await db(); 10 | const app = express(); 11 | const session = require("express-session"); 12 | require("./strategies/discord.js"); 13 | app.use(express.json()); 14 | app.use( 15 | session({ 16 | secret: process.env.SECRET || "?", 17 | cookie: { 18 | maxAge: 60000 * 60 * 24 19 | }, 20 | saveUninitialized: false, 21 | resave: false, 22 | name: "discord.oauth2", 23 | store: MongoStore.create({ mongoUrl: `mongodb+srv://${process.env.MDB_USER}:${process.env.MDB_PASS}@${process.env.MDB_PATH}/${process.env.MDB_DBNAME}` }) 24 | }) 25 | ); 26 | app.use(passport.initialize()); 27 | app.use(passport.session()); 28 | app.use(csrf()); 29 | if(process.env.SERVESTATIC === 'true') app.use(express.static(path.join(__dirname, "../public"))); 30 | app.use("/api", require("./routes/main")); 31 | 32 | app.use(function (err, req, res, next) { 33 | if (err.code !== 'EBADCSRFTOKEN') return next(err) 34 | 35 | // handle CSRF token errors here 36 | res.status(403) 37 | res.send('form tampered with') 38 | }) 39 | app.use(function (err, req, res, next) { 40 | if (err) { 41 | console.log(err); 42 | return res.status(500).send("Something happened: " + err); 43 | } else next(); 44 | }); 45 | app.use("*", function (req, res) { 46 | if(process.env.SERVESTATIC === 'true') res.status(200).sendFile(path.join(__dirname, "../public/index.html")); 47 | else res.redirect('/'); 48 | }); 49 | const listener = app.listen(process.env.PORT, () => { 50 | console.log("Your app is listening on port " + listener.address().port); 51 | }); 52 | })().catch(err => { 53 | console.log(err); 54 | process.exit(1); 55 | }); 56 | -------------------------------------------------------------------------------- /backend/strategies/discord.js: -------------------------------------------------------------------------------- 1 | const DS = require('passport-discord').Strategy; 2 | const DiscordUser = require("../models/DiscordUser.js"); 3 | const passport = require('passport'); 4 | const OAuth2 = require("../models/OAuth2Credentials"); 5 | const utils = require("../utils/utils"); 6 | passport.serializeUser((user, done) => { 7 | done(null, user.discordId) 8 | }) 9 | 10 | passport.deserializeUser(async (id, done) => { 11 | const user = await DiscordUser.findOne({ discordId: id }); 12 | if (user) done(null, user) 13 | }) 14 | 15 | passport.use(new DS({ 16 | clientID: process.env.CLIENT_ID || "123", 17 | clientSecret: process.env.CLIENT_SECRET || "123", 18 | callback: process.env.CLIENT_REDIRECT || "https://gidget.andremor.dev/api/auth", 19 | scope: ['identify', 'guilds'] 20 | }, async (acc, ref, p, done) => { 21 | try { 22 | const eat = utils.encrypt(acc).toString(); 23 | const ert = utils.encrypt(ref).toString(); 24 | const user = await DiscordUser.findOneAndUpdate({ discordId: p.id }, { 25 | username: `${p.username}#${p.discriminator}`, 26 | avatar: p.avatar, 27 | premium_type: p.premium_type || 0 28 | }); 29 | const findCredentials = await OAuth2.findOneAndUpdate({ discordId: p.id }, { 30 | accessToken: eat, 31 | refreshToken: ert, 32 | }); 33 | if (user) { 34 | if (!findCredentials) { 35 | await OAuth2.create({ 36 | discordId: p.id, 37 | accessToken: eat, 38 | refreshToken: ert 39 | }); 40 | } 41 | done(null, user); 42 | } 43 | else { 44 | const newUser = await DiscordUser.create({ 45 | discordId: p.id, 46 | username: `${p.username}#${p.discriminator}`, 47 | avatar: p.avatar, 48 | premium_type: p.premium_type || 0 49 | }); 50 | await OAuth2.create({ 51 | discordId: p.id, 52 | accessToken: eat, 53 | refreshToken: ert 54 | }); 55 | done(null, newUser); 56 | } 57 | } 58 | catch (err) { 59 | console.log(err); 60 | done(err, null); 61 | } 62 | })) -------------------------------------------------------------------------------- /backend/utils/permissions.json: -------------------------------------------------------------------------------- 1 | { 2 | "CREATE_INSTANT_INVITE": "0x1", 3 | "KICK_MEMBERS": "0x2", 4 | "BAN_MEMBERS": "0x4", 5 | "ADMINISTRATOR":"0x8", 6 | "MANAGE_CHANNELS": "0x10", 7 | "MANAGE_GUILD":"0x20", 8 | "ADD_REACTIONS":"0x40", 9 | "VIEW_AUDIT_LOG":"0x80", 10 | "VIEW_CHANNEL":"0x400", 11 | "SEND_MESSAGES":"0x800", 12 | "SEND_TTS_MESSAGES": "0x1000", 13 | "MANAGE_MESSAGES": "0x2000", 14 | "EMBED_LINKS":"0x4000", 15 | "ATTACH_FILES":"0x8000", 16 | "READ_MESSAGE_HISTORY":"0x10000", 17 | "MENTION_EVERYONE":"0x20000", 18 | "USE_EXTERNAL_EMOJIS":"0x40000", 19 | "VIEW_GUILD_INSIGHTS":"0x80000", 20 | "CONNECT":"0x100000", 21 | "SPEAK":"0x200000", 22 | "MUTE_MEMBERS":"0x400000", 23 | "DEAFEN_MEMBERS":"0x800000", 24 | "MOVE_MEMBERS":"0x1000000", 25 | "USE_VAD":"0x2000000", 26 | "PRIORITY_SPEAKER":"0x100", 27 | "STREAM":"0x200", 28 | "CHANGE_NICKNAME":"0x4000000", 29 | "MANAGE_NICKNAMES":"0x8000000", 30 | "MANAGE_ROLES":"0x10000000", 31 | "MANAGE_WEBHOOKS":"0x20000000", 32 | "MANAGE_EMOJIS":"0x40000000" 33 | } -------------------------------------------------------------------------------- /backend/utils/utils.js: -------------------------------------------------------------------------------- 1 | const permissions = require("./permissions"); 2 | const fetch = require("node-fetch"); 3 | const OAuth2 = require("../models/OAuth2Credentials"); 4 | const CryptoJS = require("crypto-js"); 5 | const api = "https://discord.com/api/v6"; 6 | const cache = new Map(); 7 | setInterval(() => { 8 | cache.clear(); 9 | }, 240000); 10 | module.exports = { 11 | getUser: async function (userId) { 12 | const algo = cache.get(`user-${userId}`); 13 | if(algo) return algo; 14 | const res = await fetch(`${api}/users/${userId}`, { 15 | method: "GET", 16 | headers: { 17 | Authorization: `Bot ${process.env.DISCORD_TOKEN}` 18 | } 19 | }); 20 | if (res.status === 429) { 21 | const json = await res.json(); 22 | await new Promise(s => setTimeout(s, (Math.floor(parseInt(json.retry_after))))); 23 | return this.getUser(userId); 24 | } 25 | const retrys = res.headers.get("X-RateLimit-Remaining"); 26 | const retryafter = res.headers.get("X-RateLimit-Reset-After"); 27 | if (!isNaN(retrys) && !isNaN(retryafter)) { 28 | if (parseInt(retrys) < 1) await new Promise(s => setTimeout(s, (Math.floor(parseInt(retryafter) * 1000) + 0.5))); 29 | } 30 | const final = await res.json(); 31 | if(res.ok) cache.set(`user-${userId}`, final) 32 | return final; 33 | }, 34 | getPermissions: function (perm) { 35 | const permissionMap = new Map(); 36 | for (const [key, value] of Object.entries(permissions)) { 37 | if ((perm & value) == value) permissionMap.set(key, value); 38 | } 39 | return permissionMap; 40 | }, 41 | getGuilds: function (botGuilds, userGuilds) { 42 | if (!Array.isArray(botGuilds)) { 43 | console.error(botGuilds) 44 | throw new Error('"botGuilds" is not an Array'); 45 | }; 46 | if (!Array.isArray(userGuilds)) { 47 | console.error(userGuilds); 48 | throw new Error('"userGuilds" is not an Array'); 49 | }; 50 | const guildMemberPermissions = new Map(); 51 | userGuilds.forEach(guild => { 52 | const perm = this.getPermissions(guild.permissions); 53 | guildMemberPermissions.set(guild.id, perm); 54 | }); 55 | const toshow = userGuilds.filter(e => { 56 | if (!botGuilds.includes(e.id)) return; 57 | const p = guildMemberPermissions.get(e.id); 58 | if (p && p.get("ADMINISTRATOR")) return true; 59 | else return false; 60 | }); 61 | return toshow; 62 | }, 63 | getBotGuilds: async function () { 64 | const res = await fetch(process.env.FETCH + "guilds", { 65 | method: "GET", 66 | headers: { 67 | "pass": process.env.ACCESS 68 | } 69 | }); 70 | if(res.ok) return await res.json(); 71 | else throw new Error(`Status code returned ${res.status} (${res.statusText})`); 72 | }, 73 | getUserGuilds: async function (discordId) { 74 | const esto = cache.get(`userGuilds-${discordId}`); 75 | if(esto) return esto; 76 | const algo = await OAuth2.findOne({ discordId }); 77 | if (!algo) throw new Error("No credentials found! Try relogin"); 78 | const eat = algo.get('accessToken'); 79 | const decrypted = this.decrypt(eat); 80 | const acc = decrypted.toString(CryptoJS.enc.Utf8) 81 | const res = await fetch(`${api}/users/@me/guilds`, { 82 | method: "GET", 83 | headers: { 84 | Authorization: `Bearer ${acc}` 85 | } 86 | }); 87 | if (res.status === 429) { 88 | const json = await res.json(); 89 | await new Promise(s => setTimeout(s, (Math.floor(parseInt(json.retry_after))))); 90 | return this.getUserGuilds(discordId); 91 | } 92 | const retrys = res.headers.get("X-RateLimit-Remaining"); 93 | const retryafter = res.headers.get("X-RateLimit-Reset-After"); 94 | if (!isNaN(retrys) && !isNaN(retryafter)) { 95 | if (parseInt(retrys) < 1) await new Promise(s => setTimeout(s, (Math.floor(parseInt(retryafter) * 1000) + 0.5))); 96 | } 97 | const final = await res.json(); 98 | if(res.ok) cache.set(`userGuilds-${discordId}`, final); 99 | return final; 100 | }, 101 | getMember: async function (guildID, userID) { 102 | const res = await fetch(`${api}/guilds/${guildID}/members/${userID}`, { 103 | method: "GET", 104 | headers: { 105 | Authorization: `Bot ${process.env.DISCORD_TOKEN}` 106 | } 107 | }); 108 | if (res.status === 429) { 109 | const json = await res.json(); 110 | await new Promise(s => setTimeout(s, (Math.floor(parseInt(json.retry_after))))); 111 | return this.getMember(guildID, userID); 112 | } 113 | const retrys = res.headers.get("X-RateLimit-Remaining"); 114 | const retryafter = res.headers.get("X-RateLimit-Reset-After"); 115 | if (!isNaN(retrys) && !isNaN(retryafter)) { 116 | if (parseInt(retrys) < 1) await new Promise(s => setTimeout(s, (Math.floor(parseInt(retryafter) * 1000) + 0.5))); 117 | } 118 | return res.json(); 119 | }, 120 | getGuildRoles: async function (guildID) { 121 | const res = await fetch(`${api}/guilds/${guildID}/roles`, { 122 | method: "GET", 123 | headers: { 124 | Authorization: `Bot ${process.env.DISCORD_TOKEN}` 125 | } 126 | }); 127 | if (res.status === 429) { 128 | const json = await res.json(); 129 | await new Promise(s => setTimeout(s, (Math.floor(parseInt(json.retry_after))))); 130 | return this.getGuildRoles(guildID); 131 | } 132 | const retrys = res.headers.get("X-RateLimit-Remaining"); 133 | const retryafter = res.headers.get("X-RateLimit-Reset-After"); 134 | if (!isNaN(retrys) && !isNaN(retryafter)) { 135 | if (parseInt(retrys) < 1) await new Promise(s => setTimeout(s, (Math.floor(parseInt(retryafter) * 1000) + 0.5))); 136 | } 137 | return res.json(); 138 | }, 139 | getMemberRoles: async function (guildID, memberID) { 140 | const member = await this.getMember(guildID, memberID); 141 | if (member.code) return member; 142 | const roles = await this.getGuildRoles(guildID); 143 | const toshow = roles.filter(e => member.roles.includes(e.id)); 144 | return toshow; 145 | }, 146 | getGuildBans: async function (guildID) { 147 | const res = await fetch(`${api}/guilds/${guildID}/bans`, { 148 | method: "GET", 149 | headers: { 150 | Authorization: `Bot ${process.env.DISCORD_TOKEN}` 151 | } 152 | }); 153 | if (res.status === 429) { 154 | const json = await res.json(); 155 | await new Promise(s => setTimeout(s, (Math.floor(parseInt(json.retry_after))))); 156 | return this.getGuildBans(guildID); 157 | } 158 | const retrys = res.headers.get("X-RateLimit-Remaining"); 159 | const retryafter = res.headers.get("X-RateLimit-Reset-After"); 160 | if (!isNaN(retrys) && !isNaN(retryafter)) { 161 | if (parseInt(retrys) < 1) await new Promise(s => setTimeout(s, (Math.floor(parseInt(retryafter) * 1000) + 0.5))); 162 | } 163 | return res.json(); 164 | }, 165 | getGuildMembers: async function (guildID, limit = 1, after = 0) { 166 | const res = await fetch(`${api}/guilds/${guildID}/members?limit=${limit}&after=${after}`, { 167 | method: "GET", 168 | headers: { 169 | "Authorization": `Bot ${process.env.DISCORD_TOKEN}` 170 | } 171 | }); 172 | if (res.status === 429) { 173 | const json = await res.json(); 174 | await new Promise(s => setTimeout(s, (Math.floor(parseInt(json.retry_after))))); 175 | return this.getGuildMembers(guildID, limit, after); 176 | } 177 | const retrys = res.headers.get("X-RateLimit-Remaining"); 178 | const retryafter = res.headers.get("X-RateLimit-Reset-After"); 179 | if (!isNaN(retrys) && !isNaN(retryafter)) { 180 | if (parseInt(retrys) < 1) await new Promise(s => setTimeout(s, (Math.floor(parseInt(retryafter) * 1000) + 0.5))); 181 | } 182 | return res.json(); 183 | }, 184 | createMessage: async function (channelID, content) { 185 | const res = await fetch(`${api}/channels/${channelID}/messages`, { 186 | method: "POST", 187 | headers: { 188 | Authorization: `Bot ${process.env.DISCORD_TOKEN}`, 189 | 'Content-Type': 'application/json' 190 | }, 191 | body: JSON.stringify(content) 192 | }); 193 | if (res.status === 429) { 194 | const json = await res.json(); 195 | await new Promise(s => setTimeout(s, (Math.floor(parseInt(json.retry_after))))); 196 | return this.createMessage(channelID, content); 197 | } 198 | const retrys = res.headers.get("X-RateLimit-Remaining"); 199 | const retryafter = res.headers.get("X-RateLimit-Reset-After"); 200 | if (!isNaN(retrys) && !isNaN(retryafter)) { 201 | if (parseInt(retrys) < 1) await new Promise(s => setTimeout(s, (Math.floor(parseInt(retryafter) * 1000) + 0.5))); 202 | } 203 | return await res.json(); 204 | }, 205 | getGuildChannels: async function (guildID) { 206 | const res = await fetch(`${api}/guilds/${guildID}/channels`, { 207 | method: "GET", 208 | headers: { 209 | Authorization: `Bot ${process.env.DISCORD_TOKEN}` 210 | } 211 | }); 212 | if (res.status === 429) { 213 | const json = await res.json(); 214 | await new Promise(s => setTimeout(s, (Math.floor(parseInt(json.retry_after))))); 215 | return this.getGuildChannels(guildID); 216 | } 217 | const retrys = res.headers.get("X-RateLimit-Remaining"); 218 | const retryafter = res.headers.get("X-RateLimit-Reset-After"); 219 | if (!isNaN(retrys) && !isNaN(retryafter)) { 220 | if (parseInt(retrys) < 1) await new Promise(s => setTimeout(s, (Math.floor(parseInt(retryafter) * 1000) + 0.5))); 221 | } 222 | return res.json(); 223 | }, 224 | encrypt: function (token) { 225 | return CryptoJS.AES.encrypt(token, process.env.VERYS) 226 | }, 227 | decrypt: function (etoken) { 228 | return CryptoJS.AES.decrypt(etoken, process.env.VERYS) 229 | }, 230 | getAvatar: function (User) { 231 | if (User.avatar) { 232 | if (User.avatar.startsWith("a_")) return `https://cdn.discordapp.com/avatars/${User.discordId}/${User.avatar}.gif?size=4096` 233 | else return `https://cdn.discordapp.com/avatars/${User.discordId}/${User.avatar}.png?size=4096` 234 | } else return `https://cdn.discordapp.com/embed/avatars/${User.username.split("#")[1] % 5}.png` 235 | }, 236 | createDM: async function (recipient_id) { 237 | const res = await fetch(`${api}/users/@me/channels`, { 238 | method: "POST", 239 | headers: { 240 | Authorization: `Bot ${process.env.DISCORD_TOKEN}`, 241 | 'Content-Type': 'application/json' 242 | }, 243 | body: JSON.stringify({ recipient_id }) 244 | }); 245 | if (res.status === 429) { 246 | const json = await res.json(); 247 | await new Promise(s => setTimeout(s, (Math.floor(parseInt(json.retry_after))))); 248 | return this.createDM(recipient_id); 249 | } 250 | const retrys = res.headers.get("X-RateLimit-Remaining"); 251 | const retryafter = res.headers.get("X-RateLimit-Reset-After"); 252 | if (!isNaN(retrys) && !isNaN(retryafter)) { 253 | if (parseInt(retrys) < 1) await new Promise(s => setTimeout(s, (Math.floor(parseInt(retryafter) * 1000) + 0.5))); 254 | } 255 | return res.json(); 256 | }, 257 | urlRegex: /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/gm 258 | }; 259 | -------------------------------------------------------------------------------- /frontend/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended' 9 | ], 10 | parserOptions: { 11 | ecmaVersion: 2020 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gidgetdashboard-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "bulmaswatch": "^0.8.1", 12 | "vue": "^2.6.11", 13 | "vue-router": "^3.5.1" 14 | }, 15 | "devDependencies": { 16 | "@vue/cli-plugin-eslint": "~4.5.13", 17 | "@vue/cli-plugin-router": "~4.5.13", 18 | "@vue/cli-service": "~4.5.13", 19 | "axios": "^0.21.1", 20 | "eslint": "^7.28.0", 21 | "eslint-plugin-vue": "^7.11.1", 22 | "vue-axios": "^3.2.4", 23 | "vue-template-compiler": "^2.6.11" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndreMor8/dashboard/cb69dcc6ddf760f8025713308aef8a49ae040f39/frontend/public/avatar.jpg -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndreMor8/dashboard/cb69dcc6ddf760f8025713308aef8a49ae040f39/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Gidget's dashboard 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | Disallow: /dashboard/ 4 | Disallow: /api/ -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import 'bulmaswatch/darkly/bulmaswatch.min.css'; 2 | import Vue from 'vue'; 3 | import VueRouter from 'vue-router'; 4 | import axios from 'axios'; 5 | import VueAxios from 'vue-axios'; 6 | import App from './App.vue'; 7 | import routes from './router.js'; 8 | 9 | Vue.use(VueRouter); 10 | Vue.use(VueAxios, axios); 11 | 12 | const router = new VueRouter({ mode: 'history', routes }); 13 | new Vue(Vue.util.extend({ router }, App)).$mount('#app'); -------------------------------------------------------------------------------- /frontend/src/partials/comment.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 51 | 52 | -------------------------------------------------------------------------------- /frontend/src/partials/footer.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 60 | 61 | -------------------------------------------------------------------------------- /frontend/src/partials/guild.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 86 | 87 | -------------------------------------------------------------------------------- /frontend/src/partials/nav.vue: -------------------------------------------------------------------------------- 1 | 152 | 153 | 158 | 159 | -------------------------------------------------------------------------------- /frontend/src/router.js: -------------------------------------------------------------------------------- 1 | import MainPage from './sections/main.vue'; 2 | import StatsPage from './sections/stats.vue'; 3 | import FeedbackPage from './sections/feedback.vue'; 4 | import PendingComments from './sections/pending_comments.vue' 5 | import CommentDelete from './sections/pending_comments_delete.vue' 6 | import Guilds from './sections/guilds.vue'; 7 | import ThanksPage from './sections/thanks.vue'; 8 | import DashboardPage from './sections/dashboard.vue'; 9 | import ddp from './sections/dashboard/default.vue'; 10 | import prefix from './sections/dashboard/prefix.vue'; 11 | import levelconfig from './sections/dashboard/levelconfig.vue'; 12 | import cp from './sections/dashboard/cp.vue'; 13 | import welcome from './sections/dashboard/welcome.vue'; 14 | import page404 from './sections/404.vue'; 15 | 16 | export default [ 17 | { 18 | name: 'MainPage', 19 | path: '/', 20 | component: MainPage 21 | }, 22 | { 23 | name: 'StatsPage', 24 | path: '/stats', 25 | component: StatsPage 26 | }, 27 | { 28 | name: 'FeedbackPage', 29 | path: '/feedback', 30 | component: FeedbackPage 31 | }, 32 | { 33 | name: 'PendingComments', 34 | path: '/pending-comments', 35 | component: PendingComments 36 | }, 37 | { 38 | name: 'CommentDelete', 39 | path: '/pending-comments/delete/:id', 40 | component: CommentDelete 41 | }, 42 | { 43 | name: 'Guilds', 44 | path: '/dashboard', 45 | component: Guilds 46 | }, 47 | { 48 | name: 'ThanksPage', 49 | path: '/thanks', 50 | component: ThanksPage 51 | }, 52 | { 53 | path: '/dashboard/:guildID', 54 | component: DashboardPage, 55 | children: [ 56 | { 57 | name: 'DashboardPage', 58 | path: '', 59 | component: ddp 60 | }, 61 | { 62 | name: 'Prefix', 63 | path: 'prefix', 64 | component: prefix 65 | }, 66 | { 67 | name: 'LevelConfig', 68 | path: 'levels', 69 | component: levelconfig 70 | }, 71 | { 72 | name: 'CustomResponses', 73 | path: 'cp', 74 | component: cp 75 | }, 76 | { 77 | name: 'Welcome', 78 | path: 'welcome', 79 | component: welcome 80 | } 81 | ] 82 | }, 83 | { 84 | path: "*", 85 | component: page404 86 | } 87 | ]; -------------------------------------------------------------------------------- /frontend/src/sections/404.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/sections/dashboard.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 68 | 69 | -------------------------------------------------------------------------------- /frontend/src/sections/dashboard/cp.vue: -------------------------------------------------------------------------------- 1 | 97 | 98 | 105 | 106 | -------------------------------------------------------------------------------- /frontend/src/sections/dashboard/default.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/sections/dashboard/levelconfig.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | -------------------------------------------------------------------------------- /frontend/src/sections/dashboard/prefix.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | -------------------------------------------------------------------------------- /frontend/src/sections/dashboard/welcome.vue: -------------------------------------------------------------------------------- 1 | 162 | 163 | 175 | 176 | -------------------------------------------------------------------------------- /frontend/src/sections/feedback.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 91 | 92 | -------------------------------------------------------------------------------- /frontend/src/sections/guilds.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 34 | 35 | -------------------------------------------------------------------------------- /frontend/src/sections/main.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 50 | 51 | -------------------------------------------------------------------------------- /frontend/src/sections/pending_comments.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | -------------------------------------------------------------------------------- /frontend/src/sections/pending_comments_delete.vue: -------------------------------------------------------------------------------- 1 |