├── src ├── config.json ├── Components │ ├── question.css │ ├── successPath.js │ ├── Error.js │ ├── errorPath.js │ ├── Success.js │ ├── 404.js │ ├── Callback.js │ ├── Home.js │ ├── Question.jsx │ └── Form.js ├── Images │ ├── header.jpg │ ├── readingtunic.jpg │ └── Discord-Logo-White.svg ├── config.json.example ├── setupTests.js ├── App.test.js ├── custom-questions.json ├── index.css ├── index.js ├── Helpers │ └── jwt-helpers.js ├── App.css ├── logo.svg ├── App.js └── serviceWorker.js ├── img.png ├── img_1.png ├── img_2.png ├── img_3.png ├── webhook.png ├── webhookdemo.png ├── public ├── robots.txt ├── logo192.png ├── logo512.png ├── index.html └── manifest.json ├── .idea ├── .gitignore ├── misc.xml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml └── discord-ban-appeal.iml ├── set_config.js ├── .gitignore ├── functions ├── helpers │ ├── jwt-helpers.js │ └── discord-helpers.js ├── guild.js ├── user-checks.js ├── unban.js ├── reject-and-block.js └── send_appeal.js ├── .env.example ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── LICENSE ├── package.json ├── netlify.toml └── README.md /src/config.json: -------------------------------------------------------------------------------- 1 | {"blocked_users":["508313640554987540"]} 2 | -------------------------------------------------------------------------------- /img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcsumlin/discord-ban-appeal/HEAD/img.png -------------------------------------------------------------------------------- /img_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcsumlin/discord-ban-appeal/HEAD/img_1.png -------------------------------------------------------------------------------- /img_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcsumlin/discord-ban-appeal/HEAD/img_2.png -------------------------------------------------------------------------------- /img_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcsumlin/discord-ban-appeal/HEAD/img_3.png -------------------------------------------------------------------------------- /webhook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcsumlin/discord-ban-appeal/HEAD/webhook.png -------------------------------------------------------------------------------- /webhookdemo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcsumlin/discord-ban-appeal/HEAD/webhookdemo.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/Components/question.css: -------------------------------------------------------------------------------- 1 | .MuiFormHelperText-root { 2 | color: white !important; 3 | } 4 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcsumlin/discord-ban-appeal/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcsumlin/discord-ban-appeal/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/Images/header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcsumlin/discord-ban-appeal/HEAD/src/Images/header.jpg -------------------------------------------------------------------------------- /src/Images/readingtunic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcsumlin/discord-ban-appeal/HEAD/src/Images/readingtunic.jpg -------------------------------------------------------------------------------- /src/config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "blocked_users": [ 3 | "269277364490338315", 4 | "204792579881959424" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /set_config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const CONFIG_FILE = './src/config.json' 3 | let rawdata = fs.readFileSync(CONFIG_FILE); 4 | let config_data = JSON.parse(rawdata); 5 | config_data["repository_url"] = process.env.REPOSITORY_URL 6 | console.log(config_data) 7 | let string_data = JSON.stringify(config_data); 8 | fs.writeFileSync(CONFIG_FILE, string_data); 9 | -------------------------------------------------------------------------------- /src/custom-questions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "question": "Why were you banned?", 4 | "character_limit": 1024 5 | }, { 6 | "question": "Why do you feel you should be unbanned?", 7 | "character_limit": 1024 8 | }, { 9 | "question": "What will you do to avoid being banned in the future?", 10 | "character_limit": 1024 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # Local Netlify folder 27 | .netlify 28 | 29 | .idea -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import ReactGA from 'react-ga'; 6 | 7 | const trackingId = process.env.REACT_APP_GOOGLE_ANALYTICS_ID; 8 | if (trackingId !== "" && trackingId !== undefined) { 9 | ReactGA.initialize(trackingId); 10 | } 11 | 12 | ReactDOM.render( 13 | 14 | 15 | , 16 | document.getElementById('root') 17 | ); 18 | 19 | -------------------------------------------------------------------------------- /src/Components/successPath.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Grid from "@material-ui/core/Grid"; 3 | import {useLocation} from "react-router-dom"; 4 | 5 | const ErrorPath = () => { 6 | const query = new URLSearchParams(useLocation().search); 7 | const msg = query.get("msg"); 8 | return ( 9 | 10 |

Success!

11 |

{msg}

12 |
13 | ) 14 | } 15 | 16 | export default ErrorPath; 17 | -------------------------------------------------------------------------------- /src/Helpers/jwt-helpers.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | 3 | function createJwt(data, duration) { 4 | const options = { 5 | issuer: 'ban-appeals-backend' 6 | }; 7 | 8 | if (duration) { 9 | options.expiresIn = duration; 10 | } 11 | 12 | return jwt.sign(data, process.env.REACT_APP_JWT_SECRET, options); 13 | } 14 | 15 | function decodeJwt(token) { 16 | return jwt.verify(token, process.env.REACT_APP_JWT_SECRET); 17 | } 18 | 19 | module.exports = { createJwt, decodeJwt }; -------------------------------------------------------------------------------- /functions/helpers/jwt-helpers.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | 3 | function createJwt(data, duration) { 4 | const options = { 5 | issuer: 'ban-appeals-backend' 6 | }; 7 | 8 | if (duration) { 9 | options.expiresIn = duration; 10 | } 11 | 12 | return jwt.sign(data, process.env.REACT_APP_JWT_SECRET, options); 13 | } 14 | 15 | function decodeJwt(token) { 16 | return jwt.verify(token, process.env.REACT_APP_JWT_SECRET); 17 | } 18 | 19 | module.exports = { createJwt, decodeJwt }; 20 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Components/Error.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Link} from "react-router-dom"; 3 | 4 | class Error extends Component { 5 | render() { 6 | return ( 7 |
8 |

Error {this.props.location.state.errorCode}!

9 | {this.props.location.state.errorMessage ?

{this.props.location.state.errorMessage}

: null} 10 | Return Home 11 |
12 | ); 13 | } 14 | } 15 | 16 | export default Error; -------------------------------------------------------------------------------- /src/Components/errorPath.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Grid from "@material-ui/core/Grid"; 3 | import {useLocation} from "react-router-dom"; 4 | 5 | const ErrorPath = () => { 6 | const query = new URLSearchParams(useLocation().search); 7 | const msg = query.get("msg"); 8 | return ( 9 | 10 |

Error! Something went wrong.

11 |

{msg}

12 |
13 | ) 14 | } 15 | 16 | export default ErrorPath; 17 | -------------------------------------------------------------------------------- /src/Components/Success.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import Grid from "@material-ui/core/Grid"; 3 | 4 | class Success extends Component { 5 | render() { 6 | return ( 7 | 8 |

Success! Your ban appeal has been submitted to the mods!

9 |

Please allow some time for them to review your appeal. Abusing this system will result in a perma-ban.

10 |
11 | ); 12 | } 13 | } 14 | 15 | export default Success; -------------------------------------------------------------------------------- /.idea/discord-ban-appeal.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_CLIENT_ID= //Discord Oauth Application Client ID 2 | REACT_APP_CLIENT_SECRET= //Discord Oauth Application Secret 3 | REACT_APP_WEBHOOK_URL= //The webhook you made for #ban-appeals 4 | REACT_APP_DISCORD_BOT_TOKEN= //Used to check if users are banned and unban them if you click the embed link to do so 5 | REACT_APP_GUILD_ID= //Brands the page with your server name and icon 6 | REACT_APP_JWT_SECRET= //What the tokens for unbanning users are hashed with. Basically a really long password 7 | REACT_APP_SKIP_BAN_CHECK= //Optional, skips the check that only allows submissions from users who are actually banned if set to true 8 | -------------------------------------------------------------------------------- /src/Components/404.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PageNotFound from '../Images/readingtunic.jpg' 3 | import {NavLink} from "react-router-dom"; 4 | class PageNotFoundError extends Component 5 | { 6 | render() 7 | { 8 | return ( 9 |
10 | 404 tunic 11 |

404 Page Not Found!

12 |

Looks like you went adventuring a little too far.

13 | 14 | Go Home 15 | 16 |
17 | ); 18 | } 19 | } 20 | 21 | export default PageNotFoundError; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /functions/guild.js: -------------------------------------------------------------------------------- 1 | const {getGuildInfo} = require("./helpers/discord-helpers"); 2 | 3 | exports.handler = async function (event, context) { 4 | if (event.httpMethod !== "GET") { 5 | return { 6 | statusCode: 405 7 | }; 8 | } 9 | 10 | try { 11 | let result = await getGuildInfo(process.env.REACT_APP_GUILD_ID); 12 | return { 13 | statusCode: 200, 14 | body: JSON.stringify({success: true, guild_name: result.name, guild_icon: result.icon}) 15 | }; 16 | } catch (e) { 17 | return { 18 | statusCode: 400, 19 | body: JSON.stringify({error: "Failed to get guild", message: e.message}) 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /functions/user-checks.js: -------------------------------------------------------------------------------- 1 | const { userIsBanned } = require("./helpers/discord-helpers.js"); 2 | 3 | exports.handler = async function (event, context) { 4 | if (event.httpMethod !== "GET") { 5 | return { 6 | statusCode: 405 7 | }; 8 | } 9 | 10 | var user_id = event.queryStringParameters.user_id 11 | if (user_id !== undefined) { 12 | if (process.env.REACT_APP_GUILD_ID) { 13 | if (!await userIsBanned(user_id, process.env.REACT_APP_GUILD_ID)) { 14 | return { 15 | statusCode: 200, 16 | body: JSON.stringify({is_banned: false}), 17 | }; 18 | } 19 | return { 20 | statusCode: 200, 21 | body: JSON.stringify({is_banned: true}), 22 | }; 23 | } 24 | } 25 | return { 26 | statusCode: 400 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Did you follow the steps outline in the README?** 11 | - [ ] Yes I did 12 | - [ ] No, I found them confusing 13 | 14 | **Describe the bug** 15 | A clear and concise description of what the bug is. 16 | 17 | **To Reproduce** 18 | Steps to reproduce the behavior: 19 | 1. Go to '...' 20 | 2. Click on '....' 21 | 3. Scroll down to '....' 22 | 4. See error 23 | 24 | **Expected behavior** 25 | A clear and concise description of what you expected to happen. 26 | 27 | **Screenshots** 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | **Desktop (please complete the following information):** 31 | - Browser [e.g. chrome, safari] 32 | - Version [e.g. 22] 33 | 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 J_C___ 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 | -------------------------------------------------------------------------------- /src/Components/Callback.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Redirect} from "react-router-dom"; 3 | import {oauth} from"../App" 4 | class Callback extends Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.state = {logged_in: false} 9 | } 10 | 11 | componentDidMount() { 12 | const params = new URLSearchParams(window.location.search) 13 | if (params.has('code') && params.has('state')){ 14 | oauth.tokenRequest({ 15 | code: params.get("code"), 16 | scope: "identify guilds email", 17 | grantType: "authorization_code", 18 | 19 | }).then((response) => { 20 | 21 | localStorage.setItem("access_token", response.access_token) 22 | localStorage.setItem("refresh_token", response.refresh_token) 23 | this.setState({logged_in: true}) 24 | }) 25 | } 26 | 27 | } 28 | 29 | render() { 30 | return ( 31 |
32 | {this.state.logged_in ? : null} 35 |
36 | ); 37 | } 38 | } 39 | 40 | export default Callback; -------------------------------------------------------------------------------- /src/Components/Home.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import Grid from "@material-ui/core/Grid"; 3 | import Button from "@material-ui/core/Button"; 4 | import {oauth} from"../App" 5 | import { ReactComponent as DiscordLogo } from '../Images/Discord-Logo-White.svg'; 6 | 7 | const crypto = require('crypto'); 8 | 9 | class Home extends Component { 10 | 11 | 12 | render() { 13 | const url = oauth.generateAuthUrl({ 14 | scope: ["identify", "email"], 15 | state: crypto.randomBytes(16).toString("hex"), // Be aware that randomBytes is sync if no callback is provided 16 | }); 17 | return ( 18 | 19 | 20 | 28 | 29 | 30 | ); 31 | } 32 | } 33 | 34 | export default Home; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discord-ban-appeal", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@hcaptcha/react-hcaptcha": "^0.3.7", 7 | "@material-ui/core": "^4.12.3", 8 | "@material-ui/icons": "^4.11.2", 9 | "@material-ui/lab": "^4.0.0-alpha.60", 10 | "@mdi/js": "^5.5.55", 11 | "@mdi/react": "^1.4.0", 12 | "@octokit/core": "^3.5.1", 13 | "@sendgrid/mail": "^7.6.0", 14 | "@testing-library/jest-dom": "^4.2.4", 15 | "@testing-library/react": "^9.3.2", 16 | "@testing-library/user-event": "^7.1.2", 17 | "chalk": "^5.0.0", 18 | "discord-oauth2": "^2.3.0", 19 | "dotenv": "^8.2.0", 20 | "history": "^5.0.1", 21 | "netlify-lambda": "^2.0.1", 22 | "node-fetch": "^2.6.0", 23 | "react": "^16.13.1", 24 | "react-dom": "^16.13.1", 25 | "react-ga": "^3.3.0", 26 | "react-helmet": "^6.1.0", 27 | "react-router-dom": "^5.2.0", 28 | "react-scripts": "3.4.2", 29 | "axios": "^0.19.2", 30 | "jsonwebtoken": "^8.5.1" 31 | }, 32 | "scripts": { 33 | "start": "react-scripts start", 34 | "build": "node set_config.js && react-scripts build", 35 | "test": "react-scripts test", 36 | "eject": "react-scripts eject", 37 | "start:lambda": "netlify-lambda serve functions" 38 | }, 39 | "eslintConfig": { 40 | "extends": "react-app" 41 | }, 42 | "browserslist": { 43 | "production": [ 44 | ">0.2%", 45 | "not dead", 46 | "not op_mini all" 47 | ], 48 | "development": [ 49 | "last 1 chrome version", 50 | "last 1 firefox version", 51 | "last 1 safari version" 52 | ] 53 | }, 54 | "proxy": "http://localhost:9000/" 55 | } 56 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "build" 3 | functions = "./functions/" 4 | 5 | [build.environment] 6 | AWS_LAMBDA_JS_RUNTIME = "nodejs12.x" 7 | 8 | [build.processing] 9 | html = { pretty_urls = true } 10 | images = { compress = true } 11 | [[redirects]] 12 | from = "/*" 13 | to = "/index.html" 14 | status = 200 15 | 16 | 17 | 18 | [template.environment] 19 | REACT_APP_CLIENT_ID= "Your Discord Application Client ID" 20 | REACT_APP_CLIENT_SECRET= "Your Discord Application Client Secret" 21 | REACT_APP_DISCORD_BOT_TOKEN="Your Discord Application Bot Token" 22 | REACT_APP_GUILD_ID="The Guild ID you're running this page for" 23 | REACT_APP_JWT_SECRET="Generate a long password (you dont need to remember this)" 24 | REACT_APP_SKIP_BAN_CHECK="Allow non-banned members to submit an appeal? (true/false)" 25 | REACT_APP_BANNER_URL="The image URL you want to use for a custom banner (Optional)" 26 | REACT_APP_SITE_TITLE="SEO title you want to use for your website" 27 | REACT_APP_SITE_DESCRIPTION="SEO Description for your site" 28 | APPEALS_CHANNEL="The id of the channel where you want appeals to appear" 29 | REACT_APP_ENABLE_HCAPTCHA="Do you want to use hCaptcha? (true|false)" 30 | REACT_APP_HCAPTCHA_SITE_KEY="Site Key provided by hCaptcha" 31 | REACT_APP_HCAPTCHA_SECRET_KEY="Your hCaptcha profile secret" 32 | REACT_APP_GOOGLE_ANALYTICS_ID="Google Analytics Tracking ID like UA-000000-01." 33 | REACT_APP_ENABLE_SENDGRID="Sends users an email when they are unbanned (true/false)" 34 | SENDGRID_API_KEY="API Key for Sendgrid" 35 | SENDGRID_SENDER_EMAIL="Single Sender Verification Email" 36 | INVITE_URL="Discord invite that can be used in email template to unbanned users" 37 | GITHUB_PAT="Github Personal Access Token for Deny and Block to work" 38 | #End of file 39 | -------------------------------------------------------------------------------- /src/Components/Question.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import InputLabel from "@material-ui/core/InputLabel"; 3 | import TextField from "@material-ui/core/TextField"; 4 | import './question.css' 5 | 6 | class Question extends Component { 7 | state = { 8 | chars_used: 0, 9 | character_limit: 1024 10 | } 11 | 12 | constructor(props) { 13 | super(props); 14 | this.handleWordCount = this.handleWordCount.bind(this); 15 | } 16 | componentDidMount() { 17 | if (this.props.characterLimit > 1024) { 18 | this.setState({character_limit: 1024}) 19 | } else { 20 | this.setState({character_limit: this.props.characterLimit}) 21 | 22 | } 23 | } 24 | 25 | 26 | handleWordCount(event) { 27 | const charCount = event.target.value.length; 28 | this.setState({chars_used: charCount}); 29 | this.props.handleChange(event) 30 | } 31 | 32 | render() { 33 | return ( 34 | 35 | 36 | {this.props.question} 37 | 38 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | export default Question; 51 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | .App-header { 11 | background-color: #282c34; 12 | min-height: 100vh; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | font-size: calc(10px + 2vmin); 18 | color: white; 19 | } 20 | 21 | .textarea { 22 | color: #dcddde !important; 23 | 24 | background: rgba(0, 0, 0, 0.1) !important; 25 | border: 1px solid rgba(0, 0, 0, 0.3) !important; 26 | 27 | outline: none !important; 28 | box-shadow: none !important; 29 | 30 | transition: border-color .2s ease-in-out; 31 | resize: none; 32 | margin: 15px 0 !important; 33 | } 34 | .MuiOutlinedInput-input, 35 | .MuiFormLabel-root { 36 | color: white !important; 37 | } 38 | 39 | body { 40 | background: #2c2f33; 41 | font-family: 'Fira Sans', sans-serif; 42 | color: #ffffff; 43 | text-align: center; 44 | } 45 | 46 | 47 | .icon { 48 | background-color: #23272a; 49 | border-radius: 25%; 50 | box-shadow: 5px 5px 10px #2c2f33; 51 | } 52 | .avatar img { 53 | border-radius: 50%; 54 | box-shadow: 5px 5px 10px #2c2f33; 55 | } 56 | .avatar, label { 57 | text-align: justify; 58 | } 59 | .button { 60 | background-color: #5865F2 !important; 61 | width: 15em; 62 | color: white !important; 63 | } 64 | 65 | button { 66 | background-color: #5865F2 !important; 67 | width: 10em; 68 | } 69 | form button { 70 | float: right; 71 | color: white !important; 72 | width: 5em !important; 73 | } 74 | 75 | .background { 76 | margin: 25px !important; 77 | /*background-color: #23272a;*/ 78 | } 79 | 80 | .banner { 81 | background-color: #23272a; 82 | background-repeat: no-repeat; 83 | background-size: cover; 84 | width: 100%; 85 | padding: 25px; 86 | border-radius: 10px; 87 | margin-top: 15px; 88 | } 89 | .form { 90 | width: 50%; 91 | } 92 | 93 | h1 { 94 | width: 100%; 95 | } 96 | -------------------------------------------------------------------------------- /src/Images/Discord-Logo-White.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /functions/unban.js: -------------------------------------------------------------------------------- 1 | const { decodeJwt } = require("./helpers/jwt-helpers.js"); 2 | const { unbanUser } = require("./helpers/discord-helpers.js"); 3 | const sendGridMail = require("@sendgrid/mail"); 4 | 5 | 6 | async function sendUnbanEmail(usersInfo, url) { 7 | sendGridMail.setApiKey(process.env.SENDGRID_API_KEY); 8 | const html = ` 9 |
10 | Hi ${usersInfo.username}#${usersInfo.user_discriminator}!
11 | Your ban appeal request submitted on ${url} has been approved!
12 | You are now able to rejoin us using this invite ${process.env.INVITE_URL} 13 |
14 | `; 15 | const mail = { 16 | from: process.env.SENDGRID_SENDER_EMAIL, 17 | to: usersInfo.email, 18 | subject: "Your Ban Appeal Was Approved!", 19 | html, 20 | }; 21 | await sendGridMail.send(mail); 22 | } 23 | 24 | exports.handler = async function (event, context) { 25 | if (event.httpMethod !== "GET") { 26 | return { 27 | statusCode: 405 28 | }; 29 | } 30 | 31 | if (event.queryStringParameters.token !== undefined) { 32 | const unbanInfo = decodeJwt(event.queryStringParameters.token); 33 | console.log(unbanInfo) 34 | if (unbanInfo.user_id !== undefined) { 35 | try { 36 | // let guild = await getGuildInfo(process.env.REACT_APP_GUILD_ID); 37 | let response = await unbanUser(unbanInfo.user_id, process.env.REACT_APP_GUILD_ID); 38 | if (response.response && response.response.data.code === 10026) { 39 | throw new Error("User is not actually banned") 40 | } 41 | let success_message = "This ban appeal has been approved and the user has been unbanned from your server" 42 | if (process.env.REACT_APP_ENABLE_SENDGRID) { 43 | await sendUnbanEmail(unbanInfo, event.headers.host) 44 | success_message += " and notified via email that they can rejoin with the provided invite" 45 | } 46 | success_message += "." 47 | return { 48 | statusCode: 302, 49 | headers: {"Location": `/success?msg=${encodeURIComponent(success_message)}`} 50 | }; 51 | } catch (e) { 52 | return { 53 | statusCode: 302, 54 | headers: {"Location": `/error?msg=${encodeURIComponent(e.message)}`} 55 | }; 56 | } 57 | } 58 | } 59 | return { 60 | statusCode: 400 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /functions/reject-and-block.js: -------------------------------------------------------------------------------- 1 | const { decodeJwt } = require("./helpers/jwt-helpers.js"); 2 | const { Octokit } = require("@octokit/core"); 3 | const {default: axios} = require("axios"); 4 | const config = require("../src/config.json") 5 | 6 | async function get_repo_info() { 7 | let regex_repo = /^https:\/\/github.com\/(?[A-Za-z.\-_0-9]+)\/(?[A-Za-z.\-_0-9]+)$/g; 8 | let repo_info = regex_repo.exec(config.repository_url) 9 | if (repo_info.groups === undefined) { 10 | throw new Error("Unable to parse repo url: " + config.repository_url) 11 | } 12 | return repo_info.groups 13 | } 14 | 15 | async function blockUser(user_id) { 16 | const octokit = new Octokit({ auth: process.env.GITHUB_PAT }); 17 | let repo_info = await get_repo_info(); 18 | let file = await octokit.request(`GET /repos/${repo_info.username}/${repo_info.repo}/contents/src/config.json`) 19 | let config_file_content = await axios({ 20 | url: `https://raw.githubusercontent.com/${repo_info.username}/${repo_info.repo}/master/src/config.json`, 21 | method: 'GET', 22 | responseType: 'blob', 23 | }) 24 | let config = config_file_content.data 25 | if (config.blocked_users.includes(user_id)) { 26 | throw new Error("User is already blocked"); 27 | } 28 | config.blocked_users.push(user_id); 29 | try { 30 | await octokit.request(`PUT /repos/${repo_info.username}/${repo_info.repo}/contents/src/config.json`, { 31 | message: 'User Blocked by API', 32 | content: Buffer.from(JSON.stringify(config)).toString('base64'), 33 | sha: file.data.sha 34 | }) 35 | } catch (e) { 36 | throw new Error("Failed to update /src/config.json: " + e.message) 37 | } 38 | 39 | } 40 | 41 | exports.handler = async function (event, context) { 42 | if (event.httpMethod !== "GET") { 43 | return { 44 | statusCode: 405 45 | }; 46 | } 47 | if (event.queryStringParameters.token !== undefined) { 48 | const unbanInfo = decodeJwt(event.queryStringParameters.token); 49 | if (unbanInfo.user_id !== undefined) { 50 | try { 51 | await blockUser(unbanInfo.user_id); 52 | return { 53 | statusCode: 302, 54 | headers: {"Location": `/success?msg=${encodeURIComponent("User blocked successfully!")}`} 55 | }; 56 | } catch (e) { 57 | return { 58 | statusCode: 302, 59 | headers: {"Location": `/error?msg=${encodeURIComponent(e.message)}`} 60 | }; 61 | } 62 | } 63 | } 64 | return { 65 | statusCode: 400 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /functions/helpers/discord-helpers.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const API_ENDPOINT = "https://discord.com/api/v9"; 3 | const MAX_EMBED_FIELD_CHARS = 1024; 4 | 5 | const instance = { 6 | baseURL: 'https://discord.com/api/v9', 7 | headers: { 8 | 'Authorization': `Bot ${process.env.REACT_APP_DISCORD_BOT_TOKEN}`, 9 | } 10 | } 11 | 12 | async function callBanApi(userId, guildId, method) { 13 | let config = { 14 | baseURL: instance.baseURL, 15 | headers: instance.headers, 16 | method: method, 17 | url: `/guilds/${encodeURIComponent(guildId)}/bans/${encodeURIComponent(userId)}` 18 | } 19 | return axios(config) 20 | .then((response) => { 21 | return response; 22 | }).catch((e) => { 23 | if (e.response && e.response.data.code === 10026) { 24 | console.log("User is not banned") 25 | } 26 | console.log(e.message) 27 | return e 28 | }) 29 | } 30 | 31 | async function userIsBanned(userId, guildId) { 32 | let result = await callBanApi(userId, guildId, "GET") 33 | return !(result.response && result.response.data.code === 10026); 34 | } 35 | 36 | async function unbanUser(userId, guildId) { 37 | const result = await callBanApi(userId, guildId, "DELETE"); 38 | if (result === false) { 39 | throw new Error("Failed to unban user"); 40 | } 41 | return result 42 | } 43 | 44 | async function getGuildInfo(guildId) { 45 | return await callGuildApi(guildId, "GET") 46 | } 47 | 48 | async function callGuildApi(guildId, method) { 49 | let config = { 50 | baseURL: instance.baseURL, 51 | headers: instance.headers, 52 | method: method, 53 | url: `/guilds/${encodeURIComponent(guildId)}` 54 | } 55 | return axios(config) 56 | .then((response) => { 57 | return response.data; 58 | }).catch((e) => { 59 | console.log("error", e.message); 60 | return false 61 | }) 62 | } 63 | 64 | // async function sendUnbanDM(userId, guild_name) { 65 | // await createDM(userId) 66 | // .then((res) => { 67 | // let config = { 68 | // baseURL: instance.baseURL, 69 | // headers: instance.headers, 70 | // method: "POST", 71 | // url: `/channels/${res.id}/messages`, 72 | // data: { 73 | // embed: { 74 | // title: `Your appeal has been approved.`, 75 | // description: `You may now rejoin ${guild_name}`, 76 | // color: 3066993 77 | // } 78 | // } 79 | // }; 80 | // return axios(config) 81 | // .then((response) => { 82 | // return response.data; 83 | // }).catch((e) => { 84 | // console.log("getting here") 85 | // throw new Error(e) 86 | // }) 87 | // }) 88 | // } 89 | 90 | // async function createDM(userId) { 91 | // let config = { 92 | // baseURL: instance.baseURL, 93 | // headers: instance.headers, 94 | // method: "POST", 95 | // url: `/users/@me/channels`, 96 | // data: {recipient_id: userId} 97 | // } 98 | // return axios(config) 99 | // .then((response) => { 100 | // return response.data; 101 | // }).catch((e) => { 102 | // throw new Error(e) 103 | // }) 104 | // } 105 | 106 | 107 | module.exports = {userIsBanned, unbanUser, getGuildInfo, API_ENDPOINT, MAX_EMBED_FIELD_CHARS}; 108 | -------------------------------------------------------------------------------- /functions/send_appeal.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const {decodeJwt} = require("./helpers/jwt-helpers"); 3 | const config = require("../src/config.json") 4 | const {MAX_EMBED_FIELD_CHARS} = require("./helpers/discord-helpers"); 5 | const {API_ENDPOINT} = require("./helpers/discord-helpers"); 6 | 7 | exports.handler = async function (event, context) { 8 | if (event.httpMethod !== "POST") { 9 | return { 10 | statusCode: 405 11 | }; 12 | } 13 | try { 14 | var unbanInfo = decodeJwt(event.headers.authorization); 15 | } catch (e) { 16 | return { 17 | statusCode: 403, 18 | body: JSON.stringify({message: e.message}) 19 | } 20 | } 21 | // Authorized 22 | if (config.blocked_users.includes(unbanInfo.user_id)) { 23 | return { 24 | statusCode: 302, 25 | headers: {"Location": `/error?msg=${encodeURIComponent("User is blocked")}`} 26 | }; 27 | } 28 | let data = JSON.parse(event.body) 29 | console.log(data) 30 | if (process.env.REACT_APP_ENABLE_HCAPTCHA === "true") { 31 | try { 32 | const params = new URLSearchParams(); 33 | params.append('secret', process.env.REACT_APP_HCAPTCHA_SECRET_KEY); 34 | params.append('response', data.hCaptcha.token); 35 | // let hCaptchaData = {'secret': process.env.REACT_APP_HCAPTCHA_SECRET_KEY, 'response': data.hCaptcha.token} 36 | let response = await axios.post("https://hcaptcha.com/siteverify", params) 37 | if (!response.data.success) { 38 | return { 39 | statusCode: 400, 40 | body: JSON.stringify({error: "Captcha failed verification"}) 41 | }; 42 | } 43 | } catch (e) { 44 | return { 45 | statusCode: 400, 46 | body: JSON.stringify({error: "Captcha failed verification", error_message: e.message}) 47 | }; 48 | } 49 | } 50 | 51 | var appeal_channel_id = process.env.APPEALS_CHANNEL; 52 | var body = { 53 | embed: { 54 | title: "New Ban Appeal Received", 55 | description: `**Username**: <@${unbanInfo.user_id}> (${unbanInfo.username}#${unbanInfo.user_discriminator})`, 56 | author: { 57 | name: unbanInfo.username, 58 | icon_url: unbanInfo.avatar_url ? unbanInfo.avatar_url : "https://discordapp.com/assets/322c936a8c8be1b803cd94861bdfa868.png" 59 | }, 60 | fields: [], 61 | timestamp: new Date().toISOString() 62 | } 63 | }; 64 | for (let i = 0; i < data.form.length; i++) { 65 | let question = data.form[i].question; 66 | let answer = data.form[i].answer.slice(0, MAX_EMBED_FIELD_CHARS); 67 | body.embed.fields.push({name: `**${question}**`, value: answer, inline: false}); 68 | } 69 | body.components = [{ 70 | type: 1, 71 | components: [ 72 | { 73 | type: 2, 74 | style: 5, 75 | label: "Approve and Unban", 76 | url: `${data.unban_url}?token=${encodeURIComponent(event.headers.authorization)}` 77 | }, 78 | { 79 | type: 2, 80 | style: 5, 81 | label: "Deny and Block", 82 | url: `${data.deny_and_block_url}?token=${encodeURIComponent(event.headers.authorization)}` 83 | }, 84 | ] 85 | }] 86 | console.log("Discord webhook body") 87 | console.log(body) 88 | 89 | return await axios.post(`${API_ENDPOINT}/channels/${encodeURIComponent(appeal_channel_id)}/messages`, 90 | body, 91 | { 92 | headers: { 93 | 'Content-Type': "application/json", 94 | "Authorization": `Bot ${process.env.REACT_APP_DISCORD_BOT_TOKEN}` 95 | } 96 | }) 97 | .then((res) => { 98 | console.log(res.data) 99 | return { 100 | statusCode: 200, 101 | body: JSON.stringify({success: true}) 102 | }; 103 | }) 104 | .catch(err => { 105 | console.log(err) 106 | return { 107 | statusCode: 500, 108 | body: JSON.stringify({ 109 | success: false, 110 | error: "Failed to post message to appeals channel using bot token. Please contact and admin or open a ticket here https://github.com/jcsumlin/discord-ban-appeal/issues/new?template=bug_report.md", 111 | }) 112 | }; 113 | }) 114 | } 115 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import './App.css'; 3 | import { 4 | BrowserRouter as Router, 5 | Switch, 6 | Route, 7 | } from "react-router-dom"; 8 | import Box from "@material-ui/core/Box"; 9 | import Home from "./Components/Home"; 10 | import Callback from "./Components/Callback"; 11 | import Form from "./Components/Form"; 12 | import {Redirect} from "react-router-dom"; 13 | import Grid from "@material-ui/core/Grid"; 14 | import Success from "./Components/Success"; 15 | import Error from "./Components/Error"; 16 | import PageNotFoundError from "./Components/404"; 17 | import Helmet from "react-helmet"; 18 | import Skeleton from '@material-ui/lab/Skeleton'; 19 | import {createBrowserHistory} from "history"; 20 | import * as ReactGA from "react-ga"; 21 | import ErrorPath from "./Components/errorPath"; 22 | import SuccessPath from "./Components/successPath"; 23 | 24 | const axios = require("axios") 25 | 26 | const DiscordOauth2 = require("discord-oauth2"); 27 | 28 | const history = createBrowserHistory(); 29 | history.listen(location => { 30 | ReactGA.set({ page: location.pathname }); // Update the user's current page 31 | ReactGA.pageview(location.pathname); // Record a pageview for the given page 32 | }); 33 | 34 | 35 | 36 | function App() { 37 | const [icon, setIcon] = useState("https://discord.com/assets/2c21aeda16de354ba5334551a883b481.png"); 38 | const [title, setTitle] = useState(null); 39 | const [loading, setLoading] = useState(true) 40 | 41 | useEffect(() => { 42 | axios.get("/.netlify/functions/guild") 43 | .then((response) => { 44 | if (response.status === 200) { 45 | setIcon(`https://cdn.discordapp.com/icons/${process.env.REACT_APP_GUILD_ID}/${response.data.guild_icon}.png`) 46 | setTitle(response.data.guild_name) 47 | setLoading(false) 48 | } else { 49 | alert("Unable to fetch server from API. Please check all your environment variables.") 50 | } 51 | }) 52 | }, []) 53 | 54 | return ( 55 | 56 | 57 | 58 | {process.env.REACT_APP_SITE_TITLE ? process.env.REACT_APP_SITE_TITLE : `${title} Discord Ban Appeal Application`} 59 | 61 | 62 | 63 | 69 | 70 | 71 | {loading ? : 72 | {title} 73 | {loading ? :

{title} Discord Ban Appeal System

} 74 |
75 |
76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | }/> 84 | 85 | 86 | 87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | ); 99 | } 100 | 101 | function PrivateRoute({children, ...rest}) { 102 | return ( 103 | 106 | localStorage.getItem("access_token") ? ( 107 | children 108 | ) : ( 109 | 115 | ) 116 | } 117 | /> 118 | ); 119 | } 120 | 121 | export default App; 122 | 123 | export const oauth = new DiscordOauth2({ 124 | clientId: process.env.REACT_APP_CLIENT_ID, 125 | clientSecret: process.env.REACT_APP_CLIENT_SECRET, 126 | redirectUri: window.location.origin + "/callback", 127 | }); 128 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Components/Form.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {oauth} from "../App" 3 | import '../App.css'; 4 | import Grid from "@material-ui/core/Grid"; 5 | import Button from "@material-ui/core/Button"; 6 | import {Redirect} from "react-router-dom"; 7 | import Question from "./Question"; 8 | import {createJwt} from "../Helpers/jwt-helpers"; 9 | import config from "../config.json" 10 | import HCaptcha from '@hcaptcha/react-hcaptcha'; 11 | import ReactGA from "react-ga"; 12 | 13 | const axios = require("axios") 14 | let questions = require('../custom-questions.json'); 15 | 16 | 17 | class Form extends Component { 18 | constructor(props) { 19 | super(props); 20 | this.state = { 21 | success: false, 22 | avatar_url: "https://discordapp.com/assets/322c936a8c8be1b803cd94861bdfa868.png", 23 | user: {id: null, avatar: null, username: null, discriminator: null, email: null}, 24 | notBanned: false, 25 | blocked: false, 26 | form: [], 27 | token: "" 28 | } 29 | this.updateState = this.updateState.bind(this); 30 | this.handleSubmit = this.handleSubmit.bind(this); 31 | } 32 | 33 | updateState(e) { 34 | let form = this.state.form 35 | var existing = false; 36 | for (let i = 0; i < form.length; i++) { 37 | if (form[i].id === e.target.id) { 38 | existing = true 39 | form[i].answer = e.target.value 40 | break 41 | } 42 | } 43 | if (existing === false) { 44 | let question = { 45 | id: e.target.id, 46 | question: e.target.name, 47 | answer: e.target.value 48 | } 49 | form.push(question) 50 | } 51 | this.setState({form: form}); 52 | } 53 | 54 | handleSubmit(e) { 55 | e.preventDefault(); 56 | if (process.env.REACT_APP_ENABLE_HCAPTCHA === "true" && this.state.token === "") { 57 | return alert("You must complete hCaptcha to submit this form") 58 | } 59 | let user_info = { 60 | username: this.state.user.username, 61 | user_id: this.state.user.id, 62 | email: this.state.user.email, 63 | user_discriminator: this.state.user.discriminator, 64 | avatar_url: this.state.user.avatar_url 65 | }; 66 | let unbanUrl = window.location.origin + "/.netlify/functions/unban"; 67 | let denyAndBlockUrl = window.location.origin + "/.netlify/functions/reject-and-block"; 68 | let data = { 69 | form: this.state.form, 70 | unban_url: unbanUrl, 71 | deny_and_block_url: denyAndBlockUrl, 72 | hCaptcha: { 73 | token: this.state.token 74 | } 75 | } 76 | let auth_header = createJwt(user_info) 77 | axios.post('/.netlify/functions/send_appeal', data, {headers: {"Authorization": auth_header}}) 78 | .then((res) => { 79 | this.setState({success: res.data.success}) 80 | }) 81 | .catch((e) => { 82 | alert(e.response.data.error) 83 | }) 84 | .finally(() => { 85 | ReactGA.event({ 86 | category: "Submit Ban Appeal", 87 | action: "User submitted a ban appeal", 88 | }); 89 | }) 90 | } 91 | 92 | componentDidMount() { 93 | oauth.getUser(localStorage.getItem("access_token")) 94 | .then(user => { 95 | if (config.blocked_users.includes(user.id)) { 96 | return this.setState({blocked: true}) 97 | } 98 | ReactGA.set({ 99 | userId: user.id, 100 | }) 101 | return user 102 | }) 103 | .then((user) => { 104 | if (process.env.REACT_APP_SKIP_BAN_CHECK === "false") { 105 | axios.get("/.netlify/functions/user-checks?user_id=" + user.id) 106 | .then((response) => { 107 | if (!response.data.is_banned) { 108 | this.setState({notBanned: true}) 109 | } 110 | }) 111 | } 112 | this.setState({user: user}) 113 | if (this.state.user.avatar) { 114 | this.setState({avatar_url: "https://cdn.discordapp.com/avatars/" + this.state.user.id + "/" + this.state.user.avatar + ".png"}) 115 | } 116 | }); 117 | } 118 | 119 | handleVerificationSuccess(token) { 120 | return this.setState({token: token}) 121 | } 122 | 123 | handleExpiration() { 124 | return this.setState({token: ""}) 125 | } 126 | 127 | render() { 128 | if (this.state.success) { 129 | return ; 130 | } 131 | if (this.state.notBanned) { 132 | return ; 136 | } 137 | if (this.state.blocked) { 138 | return ; 142 | } 143 | 144 | return ( 145 | 146 | 152 | 153 | {"Your 154 |

{this.state.user.username}#{this.state.user.discriminator}

155 |
156 | 157 | 158 |
159 | {questions ? questions.map((q, index) => { 160 | return 162 | }) : null} 163 | { 164 | process.env.REACT_APP_ENABLE_HCAPTCHA === "true" ? 165 | this.handleVerificationSuccess(token)} 168 | onExpire={() => this.handleExpiration}/> : null 169 | } 170 | 171 | 172 |
173 | 174 |
175 |
176 |
177 | ); 178 | } 179 | } 180 | 181 | export default Form; 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Project Status: [![Netlify Status](https://api.netlify.com/api/v1/badges/d045037a-6ed3-45a5-bfe5-b0d6e06bede9/deploy-status)](https://app.netlify.com/sites/tunic-ban-appeal/deploys) 3 | 4 | Inspired by [sylveon](https://github.com/sylveon/discord-ban-appeals) 5 | 6 | # [Demo](https://wumpus-ban-appeal.netlify.app) 7 | ## [Support Discord Server](https://discord.gg/EnKHckh6d2) 8 | 9 | ##### Table of Contents 10 | 1. [ Deploy on Netlify ](#netlify) 11 | 2. [ Deploy on your own web server ](#vps) 12 | 3. [ How to block users ](#block) 13 | 4. [ How to create your own custom questions ](#questions) 14 | 5. [ Adding Email Functionality to appeals form ](#emails) 15 | 6. [ Generating a Personal Access Token ](#pat) 16 | 7. [ Deny and Block Feature ](#deny) 17 | 8. [ Differences between this repo and sylveon's ](#diff) 18 | 9. [ Feature Roadmap ](#featureplan) 19 | 20 | 21 | ![Home page](img_2.png) 22 | ![](img_1.png) 23 | ![webhook in action](img.png) 24 | ![user blocked](img_3.png) 25 | 26 | # How to use this project: 27 | 28 | **REQUIREMENTS** 29 | 30 | - Have a server where you are able to: 31 | - Make channels 32 | - Create Webhooks 33 | - Invite bots 34 | 35 | 36 | ## Easy Way: Deploy on Netlify 37 | 38 | [![](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/jcsumlin/discord-ban-appeal) 39 | 40 | > **NOTE**: If you already have a custom bot in your server and access to its credentials, skip the first step 41 | - Create a custom bot inside your server. You can register/invite one [here](https://discord.com/developers/applications). Keep that window handy. 42 | - Click the "Deploy to Netlify" button. 43 | - You will be asked to link your GitHub account then enter values for all the environment variables. (See Environment Variable Information Table) 44 | - Set the environment variables from your Discord bot application page (`REACT_APP_CLIENT_ID`, `REACT_APP_CLIENT_SECRET`, `REACT_APP_DISCORD_BOT_TOKEN`) 45 | - Choose a channel (or create a new one) where you want all the ban appeals to appear and copy its ID into `APPEALS_CHANNEL` 46 | - Copy your server's ID into `REACT_APP_GUILD_ID` 47 | - Make a random JWT Secret or generator one [here](https://1password.com/password-generator/) and set inside `REACT_APP_JWT_SECRET` 48 | - Set `REACT_APP_ENABLE_HCAPTCHA` to `false` unless you intend to add hCaptcha 49 | - Set `REACT_APP_ENABLE_SENDGRID` to `false` unless you intend to use Sendgrid for unban notifications. 50 | - Make and set the `GITHUB_PAT` (see table for information on how) 51 | - Deploy your application 52 | - Lastly we'll want to make sure users can login using Discord 53 | - First make any changes to the netlify.app deployment URL you wish, or set up your own custom one! 54 | - From the [Discord Developer Application page](https://discord.com/developers/applications) Select the OAuth tab 55 | - Click on Add Redirect and enter `https://[site-url]/callback` where `[site-url]` is the site name netlify assigned you, or the one you changed it to. 56 | 57 | ### Environment Variable Information 58 | 59 | | Environment Variable | Description | Optional? | 60 | |-------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------| 61 | | REACT_APP_CLIENT_ID | Client ID of a Discord Application | No | 62 | | REACT_APP_CLIENT_SECRET | Client Secret of a Discord Application | No | 63 | | REACT_APP_DISCORD_BOT_TOKEN | The Bot token of a Discord Application | No | 64 | | REACT_APP_GUILD_ID | The Server/Guild ID where you are accepting ban appeals | No | 65 | | REACT_APP_JWT_SECRET | A really long string of characters used to establish
a secure line of communication with the API of this app.
I would recommend using a password generator to create this.
**You don't have to remember what its set to** | No | 66 | | REACT_APP_SKIP_BAN_CHECK | If set to "true" the application will not check if
a user is banned before allowing them to fill out
an appeal form | Yes | 67 | | REACT_APP_BANNER_URL | Add a custom banner behind your server icon.
Must be a direct link to an image
(usually ends in .jpeg or .png etc.) | Yes | 68 | | REACT_APP_SITE_TITLE | Use a custom title for your site (defaults to {server_name}'s Discord Ban Appeal Application if none is set) | Yes | 69 | | REACT_APP_SITE_DESCRIPTION | Use a custom SEO description for your site (defaults to {server_name}'s Discord Ban Appeal Application if none is set) | Yes | 70 | | APPEALS_CHANNEL | The channel where you want appeals to appear in | No | 71 | | REACT_APP_ENABLE_HCAPTCHA | Do you want to use hCaptcha in the form? (true/false) | Yes | 72 | | REACT_APP_HCAPTCHA_SITE_KEY | The hCaptcha site key generated by hCaptcha | Yes | 73 | | REACT_APP_HCAPTCHA_SECRET_KEY | The secret on your hCaptcha profile | Yes | 74 | | REACT_APP_GOOGLE_ANALYTICS_ID | Google Analytics Tracking ID like UA-000000-01. | Yes | 75 | | REACT_APP_ENABLE_SENDGRID | Sends users an email when they are unbanned (true/false) See Wiki if you don't know how to set this up | No | 76 | | SENDGRID_API_KEY | [API Key for Sendgrid](https://app.sendgrid.com/settings/api_keys) | Yes | 77 | | SENDGRID_SENDER_EMAIL | [Single Sender Verification Email](https://docs.sendgrid.com/ui/sending-email/sender-verification) | Yes | 78 | | INVITE_URL | Discord invite that can be used in email template to unbanned users | Yes | 79 | | GITHUB_PAT | [Github Personal Access Token](https://github.com/settings/tokens/new) for Deny and Block feature to work. Make sure it never expires and to select the `repo` scope | No | 80 | 81 | 82 | ## Hard Way: Deploy on your own web server 83 | 84 | This if by far not the prettiest way to do this which is why I recommend you use netlify, but if you're smart enough to deploy this on your own then go for it! 85 | 86 | ### Requirements: 87 | 88 | Be aware this project uses serverless functions as its API layer. 89 | All the API requests are directed at /.netlify/functions because support issues with netlify's redirect rules. 90 | To deploy this yourself you will need to create a serverless API using AWS Lambda or an equivalent from Azure or GCP. 91 | I will go into specifics below. 92 | 93 | ### Web frontend 94 | - Fork this repo 95 | - Copy `.env.example` to `.env` and fill in each value 96 | - Run `yarn install` to install the dependencies 97 | - Run `yarn build` to compile a production build 98 | - Direct your webserver to serve the `./build/` directory 99 | 100 | ### Serverless backend 101 | 102 | - Create a new serverless API in your cloud provider with 4 endpoints. 103 | - Each File in `/functions` will be an endpoint, and most of them will require both the files in the `/functions/helpers` folder 104 | - Make sure all the packages from `package.json` are installed and available for each function 105 | - Find and replace all occurrences of `/.netlify/functions/` with your endpoint for each function 106 | 107 | I've oversimplified a lot of the serverless portion here since it will vary based on your cloud provider but this covers the jist of things. 108 | 109 | 110 | ## Adding hCaptcha (like reCaptcha) 111 | [See Wiki article](https://github.com/jcsumlin/discord-ban-appeal/wiki/Adding-hCaptcha) 112 | 113 | ## How to block users from abusing your ban appeal form. 114 | [See Wiki article](https://github.com/jcsumlin/discord-ban-appeal/wiki/Blocking-users-from-submitting-ban-appeals) 115 | 116 | 117 | ## How to create your own custom questions. 118 | [See Wiki article](https://github.com/jcsumlin/discord-ban-appeal/wiki/How-to-create-your-own-custom-questions) 119 | 120 | 121 | ## Adding Email Functionality to appeals form 122 | [See Wiki article](https://github.com/jcsumlin/discord-ban-appeal/wiki/How-to-email-users-when-theyre-unbanned) 123 | 124 | 125 | ## How to generate a Personal Access Token 126 | [See Wiki article](https://github.com/jcsumlin/discord-ban-appeal/wiki/How-to-make-a-Personal-Access-Token) 127 | 128 | 129 | ## Differences between this repo and sylveon's 130 | - Server icon and custom banner on landing page 131 | - Only allow users who are actually banned to submit an appeal 132 | - Ability to disable this check 133 | - Custom meta tags for better SEO and visibility. 134 | - **IMO** a cleaner approach to custom questions. 135 | - Email integration for unban notification 136 | - Deny and block users from discord embed 137 | 138 | 139 | ## Feature roadmap 140 | - [x] Allow users to be blocked from submitting a ban appeal 141 | - [x] Add better meta tag support 142 | - [x] Custom Questions defined by the user 143 | - [x] add hCaptcha/reCaptcha 144 | - [x] Integrate some means of alerting users who are unbanned 145 | - [x] Additional Actions such as "Deny Ban appeal". 146 | - [x] Optional Google Analytics tracking 147 | --------------------------------------------------------------------------------