From 598e48e25d1e29c79f46b2554133a434ddc5c58f Mon Sep 17 00:00:00 2001
From: Akif9748 <akif9748@gmail.com>
Date: Sat, 17 Sep 2022 19:33:51 +0300
Subject: [PATCH] Added discord auth support

---
 .env.example           |  3 ++-
 .gitignore             |  3 +++
 README.md              | 14 +++++++---
 config.json            |  6 +++--
 config.json.example    | 17 +++++++++++++
 index.js               | 10 ++++++--
 models/User.js         |  1 +
 package-lock.json      |  1 +
 package.json           |  1 +
 routes/discord_auth.js | 58 ++++++++++++++++++++++++++++++++++++++++++
 routes/login.js        |  2 +-
 routes/register.js     |  2 +-
 routes/users.js        |  2 +-
 views/login.ejs        |  1 +
 views/register.ejs     |  2 ++
 views/user.ejs         |  4 ++-
 16 files changed, 115 insertions(+), 12 deletions(-)
 create mode 100644 config.json.example
 create mode 100644 routes/discord_auth.js

diff --git a/.env.example b/.env.example
index 3305854..8048631 100644
--- a/.env.example
+++ b/.env.example
@@ -1 +1,2 @@
-MONGO_DB_URL = mongodb://localhost:27017/akf-forum
\ No newline at end of file
+MONGO_DB_URL = mongodb://localhost:27017/akf-forum
+DISCORD_CLIENT = discord_app_id
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 0ef53cf..ada5835 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,9 @@ node_modules/
 # Environment variables
 .env
 
+# Config files
+config.json
+
 # Test files
 test.js
 
diff --git a/README.md b/README.md
index b2049e0..2ef19b3 100644
--- a/README.md
+++ b/README.md
@@ -4,11 +4,18 @@ A Node.js based forum software.
 ## Installation
 - Clone or download this repo.
 - Run `npm i` to install **dependencies**.
+- Enter your database credentials in `.env`.
 - Run `npm start` for run it. 
 
 ### Extra
 Run `node util/reset` to **reset the database** for duplicate key errors, and run `node util/admin` for give admin perms to first member.
-Edit `config.json` for default themes of users, and forum name...
+Edit `config.json` for default themes (`black` or `default`) of users, and forum name, meta description, character limits, discord auth enabler, global ratelimit.
+
+### DISCORD AUTH: 
+`discord_auth: true` in config.json.
+Enter application id to `.env`.
+Create a redirect url in discord developer portal:
+`https://forum_url.com/discord_auth/hash`
 
 ## API
 Akf-forum has got an API for AJAX (fetch), other clients etc. And, you can learn about API in `APIDOCS.md`.
@@ -35,11 +42,12 @@ Akf-forum has got an API for AJAX (fetch), other clients etc. And, you can learn
 | To do | Is done? | 
 | ----- | -------- | 
 | Profile Message | ⚪ | 
-| Better Auth | ⚪ | 
+| Better Auth for API way | ⚪ | 
 | mod role, permissions | ⚪ | 
 | upload other photos, model for it | ⚪ | 
-| categories page is need a update | ⚪ | 
+| categories page is need a update, thread count in category | ⚪ | 
 | preview for send messages in markdown format | ⚪ |
+| DC auth will store code for taking tokens, and create secret model setting | ⚪ |
 
 ## Major Version History
 - V4: Caching
diff --git a/config.json b/config.json
index 7d1d9ea..779f36b 100644
--- a/config.json
+++ b/config.json
@@ -8,9 +8,11 @@
         "names": 25,
         "desp": 256
     },
-    "global_ratelimit":{
+    "global_ratelimit": {
         "enabled": true,
         "max": 25,
         "windowMs": 60000
-    }
+    },
+    "discord_auth": true
+    
 }
\ No newline at end of file
diff --git a/config.json.example b/config.json.example
new file mode 100644
index 0000000..9e5eded
--- /dev/null
+++ b/config.json.example
@@ -0,0 +1,17 @@
+{
+    "def_theme": "default",
+    "forum_name": "akf",
+    "description": "Akf-forum!",
+    "limits": {
+        "title": 128,
+        "message": 1024,
+        "names": 25,
+        "desp": 256
+    },
+    "global_ratelimit": {
+        "enabled": true,
+        "max": 25,
+        "windowMs": 60000
+    },
+    "discord_auth": false
+}
\ No newline at end of file
diff --git a/index.js b/index.js
index c386ab5..e3ff07e 100644
--- a/index.js
+++ b/index.js
@@ -4,7 +4,7 @@ const { urlencoded: BP } = require('body-parser'),
     SES = require('express-session');
 
 const
-    { def_theme, forum_name, description, limits, global_ratelimit: RLS } = require("./config.json"),
+    { def_theme, forum_name, description, limits, global_ratelimit: RLS, discord_auth } = require("./config.json"),
     { UserModel, BanModel } = require("./models"),
     port = process.env.PORT || 3000,
     mongoose = require("mongoose"),
@@ -50,4 +50,10 @@ for (const file of fs.readdirSync("./routes"))
 
 app.all("*", (req, res) => res.error(404, "We have not got this page."));
 
-app.listen(port, () => console.log(`${forum_name}-forum on port:`, port));
\ No newline at end of file
+const server = app.listen(port, () => console.log(`${forum_name}-forum on port:`, port));
+
+if (discord_auth) {
+    const { address } = server.address();
+    console.log(`https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_CLIENT}&redirect_uri=http%3A%2F%2F${address == '::' ? 'localhost:' + port : address}%2Fdiscord_auth%2Fhash&response_type=token&scope=identify`)
+    app.set("discord_auth", `https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_CLIENT}&redirect_uri=http%3A%2F%2F${address == '::' ? 'localhost:' + port : address}%2Fdiscord_auth%2Fhash&response_type=token&scope=identify`);
+}
\ No newline at end of file
diff --git a/models/User.js b/models/User.js
index 5373ae7..e573646 100644
--- a/models/User.js
+++ b/models/User.js
@@ -2,6 +2,7 @@ const mongoose = require("mongoose")
 const { def_theme } = require("../config.json");
 const schema = new mongoose.Schema({
     id: { type: String, unique: true },
+    discordID: { type: String, unique: true },
     name: { type: String, maxlength: 25 },
     avatar: { type: String, default: "/images/avatars/default.jpg" },
     time: { type: Date, default: Date.now },
diff --git a/package-lock.json b/package-lock.json
index d1f4661..d63deb6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,7 @@
         "express-session": "^1.17.2",
         "mongoose": "^6.6.1",
         "multer": "^1.4.5-lts.1",
+        "node-fetch": "^2.6.7",
         "request-ip": "^3.3.0"
       },
       "engines": {
diff --git a/package.json b/package.json
index 1f7a573..a9c3179 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
     "express-session": "^1.17.2",
     "mongoose": "^6.6.1",
     "multer": "^1.4.5-lts.1",
+    "node-fetch": "^2.6.7",
     "request-ip": "^3.3.0"
   }
 }
diff --git a/routes/discord_auth.js b/routes/discord_auth.js
new file mode 100644
index 0000000..9f5c2cc
--- /dev/null
+++ b/routes/discord_auth.js
@@ -0,0 +1,58 @@
+const { Router } = require("express")
+const { UserModel } = require("../models");
+const fetch = require("node-fetch");
+const app = Router();
+
+app.get("/hash", (req, res) => res.send('<script>location.href=location.href.replace("#","?").replace("discord_auth/hash","discord_auth");</script>'))
+
+app.get("/", async (req, res) => {
+    const { access_token, token_type } = req.query;
+    if (!access_token) return;
+    try {
+        const discord = await fetch('https://discord.com/api/users/@me', {
+            headers: { authorization: `${token_type} ${access_token}` }
+        }).then(res => res.json());
+
+        const forum = await UserModel.findOne({ discordID: discord.id });
+
+
+        if (req.user) {
+            if (req.user.discordID)
+                return res.error(403, "Your forum account is already linked to a discord account.");
+
+            if (forum)
+                return res.error(403, "This discord account is already linked to a forum account.");
+
+            req.user.discordID = discord.id;
+            await req.user.save();
+            return res.send("Your discord account has been linked to your forum account.");
+        }
+
+
+        if (forum) {
+            req.session.userID = forum.id;
+            return res.redirect("/");
+        }
+        
+        let name = discord.username + discord.discriminator;
+        while (await UserModel.findOne({ name }))
+            name += Math.floor(Math.random() * 2);
+
+        const user2 = new UserModel({
+            name, discordID: discord.id,
+            avatar: `https://cdn.discordapp.com/avatars/${discord.id}/${discord.avatar}.png?size=256`
+        });
+
+        await user2.takeId();
+        await user2.save();
+
+        req.session.userID = user2.id;
+
+        res.redirect("/");
+    } catch (error) {
+        res.error(500, "Something went wrong");
+        console.error(error);
+    }
+});
+
+module.exports = app;
\ No newline at end of file
diff --git a/routes/login.js b/routes/login.js
index ce8eb19..096a345 100644
--- a/routes/login.js
+++ b/routes/login.js
@@ -3,7 +3,7 @@ const { Router } = require("express");
 const app = Router();
 const bcrypt = require("bcrypt");
 
-app.get("/", (req, res) => res.reply("login", { redirect: req.query.redirect, user: null }));
+app.get("/", (req, res) => res.reply("login", { redirect: req.query.redirect, user: null, discord: req.app.get("discord_auth") }));
 
 app.post("/", async (req, res) => {
     req.session.userID = null;
diff --git a/routes/register.js b/routes/register.js
index ec5c50d..2824094 100644
--- a/routes/register.js
+++ b/routes/register.js
@@ -4,7 +4,7 @@ const bcrypt = require("bcrypt");
 const rateLimit = require('express-rate-limit');
 const app = Router();
 
-app.get("/", (req, res) => res.reply("register", { user: null }));
+app.get("/", (req, res) => res.reply("register", { user: null, discord: req.app.get("discord_auth") }));
 
 app.post("/", rateLimit({
     windowMs: 24 * 60 * 60_000, max: 5, standardHeaders: true, legacyHeaders: false,
diff --git a/routes/users.js b/routes/users.js
index 19a7dba..840d4b6 100644
--- a/routes/users.js
+++ b/routes/users.js
@@ -27,7 +27,7 @@ app.get("/:id", async (req, res) => {
 
         const message = await MessageModel.count({ authorID: id });
         const thread = await ThreadModel.count({ authorID: id });
-        res.reply("user", { member, counts: { message, thread } })
+        res.reply("user", { member, counts: { message, thread}, discord: req.app.get("discord_auth")  })
     }
     else res.error(404, `We don't have any user with id ${id}.`);
 
diff --git a/views/login.ejs b/views/login.ejs
index 6d992d2..c291544 100644
--- a/views/login.ejs
+++ b/views/login.ejs
@@ -7,6 +7,7 @@
   <link rel="stylesheet" href="/css/login.css" />
 
   <%- include("extra/navbar") %>
+  <a href="<%=discord%>" class="btn-outline-primary">Login with discord</a>
 
   <h1 class="title">Login</h1>
 
diff --git a/views/register.ejs b/views/register.ejs
index d5dfe6c..f58ccee 100644
--- a/views/register.ejs
+++ b/views/register.ejs
@@ -8,9 +8,11 @@
   <link rel="stylesheet" href="/css/login.css" />
 
   <%- include("extra/navbar") %>
+  <a href="<%=discord%>" class="btn-outline-primary">Login with discord</a>
 
   <h1 class="title">Register</h1>
 
+
   <form action="/register" method="post">
     <input type="text" name="username" maxlength="25" placeholder="Username" class="input" required>
     <input type="password" name="password" maxlength="25" placeholder="Password" class="input" required>
diff --git a/views/user.ejs b/views/user.ejs
index 1aa1c42..276ad59 100644
--- a/views/user.ejs
+++ b/views/user.ejs
@@ -13,7 +13,9 @@
 
 
   <div class="content">
-
+    <% if (!member.discordID && user?.id === member.id) { %>
+    <a href="<%=discord%>" class="btn-outline-primary">Login with discord</a>
+    <% } %>
     <% if (user?.admin || user?.id === member.id) { %>
     <a href="/users/<%=member.id%>/avatar" class="btn-outline-primary">Upload avatar</a>
     <link rel="stylesheet" href="/css/modal.css" />