├── 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 |
5 |
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 |
4 |
5 |
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 | You need to enable JavaScript to run this app.
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 |
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 | }
22 | href={url}
23 | size={"large"}
24 | className={"button"}
25 | >
26 | Login with Discord
27 |
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 | }
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 |
154 | {this.state.user.username}#{this.state.user.discriminator}
155 |
156 |
157 |
174 |
175 |
176 |
177 | );
178 | }
179 | }
180 |
181 | export default Form;
182 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Project 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 | 
22 | 
23 | 
24 | 
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://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 |
--------------------------------------------------------------------------------