diff --git a/README.md b/README.md index ac5f6d5..72ffbca 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,13 @@ Edit `config.json` for default themes (`black` or `default`) of users, and forum `"discord_auth": "your_app_id"` in config.json. Add your app secret to `.env` as `DISCORD_SECRET`. Create a redirect url in discord developer portal: -`https://forum_url.com/discord_auth/hash` +`https://forum_url.com/auth/discord` + +### EMAIL AUTH: +You can configure it. Just edit `config.json` and `.env` files. +`"email_auth": true` in config.json. +Add your email credentials to `.env` as `EMAIL_USER` and `EMAIL_PASS`. +Add your email domain to `.env` as `EMAIL_SERVICE`. ## API Akf-forum has got an API for AJAX (fetch), other clients etc. And, you can learn about API in `APIDOCS.md`. @@ -45,14 +51,14 @@ Akf-forum has got an API for AJAX (fetch), other clients etc. And, you can learn - upload other photos, model for it - categories page is need a update, thread count in category - Disable last seen button for web. -- email auth. - old contents / titles add to forum interface - add ban button to user profile. - change password. - add approval threads page. - who liked a message for web. - edit config from web admin panel. - +- user.state for ban, delete, etc. +- Add a feature list to README.md ## Major Version History - V4: Caching - V3: New Theme diff --git a/config.json.example b/config.json.example index 1418a46..4199bf3 100644 --- a/config.json.example +++ b/config.json.example @@ -15,5 +15,6 @@ }, "discord_auth": "", "defaultThreadState": "OPEN", + "email_auth": false, "host": "https://akf-forum.glitch.me" } \ No newline at end of file diff --git a/index.js b/index.js index b268d6e..86494bb 100644 --- a/index.js +++ b/index.js @@ -22,19 +22,21 @@ app.ips = []; app.set("view engine", "ejs"); app.set("limits", limits); -app.use(express.static("public"), express.json(), express.urlencoded({extended:true}), IP(), +app.use(express.static("public"), express.json(), express.urlencoded({ extended: true }), IP(), SES({ secret: process.env.SECRET, store: MS.create({ clientPromise: DB, stringify: false }), resave: true, saveUninitialized: true }), async (req, res, next) => { if (app.ips.includes(req.clientIp)) return res.status(403).send("You are banned from this forum."); req.user = req.session.userID ? await UserModel.findOneAndUpdate({ id: req.session.userID }, { lastSeen: Date.now(), $addToSet: { ips: req.clientIp } - }): null; + }) : null; res.reply = (page, options = {}, status = 200) => res.status(status) .render(page, { user: req.user, theme: req.user?.theme || def_theme, forum_name, description, ...options }); res.error = (type, error) => res.reply("error", { type, error }, type); + if (req.user && !req.user.approved && !req.url.startsWith("/auth/email")) return res.error(403, "Your account is not approved yet."); + if (req.user?.deleted) { req.session.destroy(); return res.error(403, "Your account has been deleted."); diff --git a/lib.js b/lib.js index 3599709..de55d19 100644 --- a/lib.js +++ b/lib.js @@ -1,5 +1,7 @@ const RL = require('express-rate-limit'); - +const nodemailer = require("nodemailer"); +const config = require("./config.json"); +require("dotenv").config(); module.exports = { threadEnum: ["OPEN", "APPROVAL", "DELETED"], themes: ["default", "black"], @@ -8,6 +10,17 @@ module.exports = { windowMs, max, standardHeaders: true, legacyHeaders: false, handler: (req, res, next, opts) => !req.user?.admin ? res.error(opts.statusCode, "You are begin ratelimited") : next() }) - } + }, + emailRegEx: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, -} \ No newline at end of file + +} + +if (config.email_auth) + module.exports.transporter = nodemailer.createTransport({ + service: process.env.EMAIL_SERVICE, direct: true, secure: true, + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS + } + }); diff --git a/models/Thread.js b/models/Thread.js index f760e30..a5f3407 100644 --- a/models/Thread.js +++ b/models/Thread.js @@ -12,13 +12,12 @@ const schema = new mongoose.Schema({ title: { type: String, maxlength: limits.title }, oldTitles: [String], - + time: { type: Date, default: Date.now }, edited: { type: Boolean, default: false }, state: { type: String, default: defaultThreadState, enum: threadEnum }, messages: [String], views: { type: Number, default: 0 } - }); diff --git a/models/User.js b/models/User.js index a460f8d..25cc825 100644 --- a/models/User.js +++ b/models/User.js @@ -1,5 +1,5 @@ const mongoose = require("mongoose") -const { def_theme, limits } = require("../config.json"); +const { def_theme, limits, email_auth } = require("../config.json"); const schema = new mongoose.Schema({ id: { type: String, unique: true }, discordID: { type: String }, @@ -15,7 +15,10 @@ const schema = new mongoose.Schema({ hideLastSeen: { type: Boolean, default: false }, ips: { type: [String], default: [], select: false }, password: { type: String, select: false }, - discord_code: { type: String, select: false } + discord_code: { type: String, select: false }, + approved: { type: Boolean, default: !email_auth }, + email: { type: String, select: false }, + email_code: { type: String, select: false }, }); schema.methods.takeId = async function () { diff --git a/package-lock.json b/package-lock.json index 3ada425..aa02de3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "mongoose": "^6.6.1", "multer": "^1.4.5-lts.1", "node-fetch": "^2.6.7", + "nodemailer": "^6.7.8", "request-ip": "^3.3.0" }, "engines": { @@ -1308,6 +1309,14 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/nodemailer": { + "version": "6.7.8", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.8.tgz", + "integrity": "sha512-2zaTFGqZixVmTxpJRCFC+Vk5eGRd/fYtvIR+dl5u9QXLTQWGIf48x/JXvo58g9sa0bU6To04XUv554Paykum3g==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -2822,6 +2831,11 @@ } } }, + "nodemailer": { + "version": "6.7.8", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.8.tgz", + "integrity": "sha512-2zaTFGqZixVmTxpJRCFC+Vk5eGRd/fYtvIR+dl5u9QXLTQWGIf48x/JXvo58g9sa0bU6To04XUv554Paykum3g==" + }, "nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", diff --git a/package.json b/package.json index 9278e37..6a8f7a5 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "mongoose": "^6.6.1", "multer": "^1.4.5-lts.1", "node-fetch": "^2.6.7", + "nodemailer": "^6.7.8", "request-ip": "^3.3.0" } } diff --git a/routes/api/index.js b/routes/api/index.js index 6701cec..06ccb18 100644 --- a/routes/api/index.js +++ b/routes/api/index.js @@ -20,6 +20,7 @@ app.use(async (req, res, next) => { const user = await UserModel.findOne({ name }); if (!user || user.deleted) return res.error(401, `We don't have any user with name ${name}.`) + if (!user.approved) return res.error(401, "Your account is not approved yet."); if (!await bcrypt.compare(password, user.password)) return res.error(401, 'Incorrect Password!'); diff --git a/routes/api/routes/messages.js b/routes/api/routes/messages.js index 5368dc9..8c90043 100644 --- a/routes/api/routes/messages.js +++ b/routes/api/routes/messages.js @@ -25,7 +25,7 @@ app.delete("/:id/", async (req, res) => { if (user.id != message.authorID && !user.admin) return res.error(403, "You have not got permission for this."); - if (message.deleted) return res.error(403, "This message is already deleted."); + if (message.deleted) return res.error(404, "This message is already deleted."); message.deleted = true; await message.save() diff --git a/routes/api/routes/threads.js b/routes/api/routes/threads.js index b122ba4..f30d511 100644 --- a/routes/api/routes/threads.js +++ b/routes/api/routes/threads.js @@ -99,7 +99,7 @@ app.delete("/:id/", async (req, res) => { if (user.id != thread.authorID && !user.admin) return res.error(403, "You have not got permission for this."); - if (thread.state == "DELETED") return res.error(403, "This thread is already deleted."); + if (thread.state == "DELETED") return res.error(404, "This thread is already deleted."); thread.state = "DELETED"; await thread.save(); diff --git a/routes/auth.js b/routes/auth.js index 1926236..094bdcb 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -2,7 +2,7 @@ const { Router } = require("express") const { UserModel } = require("../models"); const fetch = require("node-fetch"); const app = Router(); -const { host, discord_auth } = require("../config.json") +const { host, discord_auth, email_auth } = require("../config.json") app.get("/discord", async (req, res) => { const client_id = discord_auth; @@ -22,7 +22,7 @@ app.get("/discord", async (req, res) => { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, - }); + }); if (!response.ok) return res.error(500, "Bad request to discord"); @@ -39,7 +39,7 @@ app.get("/discord", async (req, res) => { 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."); + return res.error(409, "This discord account is already linked to a forum account."); req.user.discordID = discord.id; req.user.discord_code = code; @@ -75,7 +75,7 @@ app.get("/discord", async (req, res) => { }); app.delete("/discord", async (req, res) => { - if (!req.user) return res.error(403, "You are not logged in"); + if (!req.user) return res.error(401, "You are not logged in"); if (!req.user.discordID) return res.error(403, "You don't have a discord account linked to your forum account."); req.user.discordID = undefined; req.user.discord_code = undefined; @@ -83,5 +83,16 @@ app.delete("/discord", async (req, res) => { res.send("Your discord account has been unlinked from your forum account."); }); +app.get("/email", async (req, res) => { + if (!email_auth) return res.error(404, "Email auth is disabled"); + if (!req.user) return res.error(401, "You are not logged in"); + if (req.user.email) return res.error(403, "You already have an email linked to your account."); + const { code } = req.query; + if (!code) return res.error(400, "No code provided"); + if (code !== req.user.email_code) return res.error(403, "Invalid code"); + req.user.approved = true; + await req.user.save(); + res.send("Your email has been linked to your forum account."); +}); module.exports = app; \ No newline at end of file diff --git a/routes/login.js b/routes/login.js index 41edcb3..cd0328c 100644 --- a/routes/login.js +++ b/routes/login.js @@ -13,8 +13,8 @@ app.post("/", async (req, res) => { if (!name || !password) return res.error(400, "You forgot entering some values") const member = await UserModel.findOne({ name }, "+password"); - if (!member || member.deleted) return res.error(403, 'Incorrect username!'); - if (!await bcrypt.compare(password, member.password)) return res.error(403, 'Incorrect password!'); + if (!member || member.deleted) return res.error(401, 'Incorrect username!'); + if (!await bcrypt.compare(password, member.password)) return res.error(401, 'Incorrect password!'); req.session.userID = member.id; diff --git a/routes/register.js b/routes/register.js index 95b5393..e098e71 100644 --- a/routes/register.js +++ b/routes/register.js @@ -1,10 +1,10 @@ const { UserModel } = require("../models"); const { Router } = require("express") const bcrypt = require("bcrypt"); -const { RL } = require('../lib'); +const { RL, transporter, emailRegEx } = require('../lib'); const app = Router(); - -app.get("/", (req, res) => res.reply("register", { user: null, discord: req.app.get("discord_auth") })); +const { email_auth, forum_name, host } = require("../config.json"); +app.get("/", (req, res) => res.reply("register", { user: null, discord: req.app.get("discord_auth"), mail: email_auth })); app.post("/", RL(24 * 60 * 60_000, 5), async (req, res) => { @@ -26,9 +26,30 @@ app.post("/", RL(24 * 60 * 60_000, 5), async (req, res) => { } await user.takeId() - if (user.id === "0") user.admin = true; + if (user.id === "0") + user.admin = true; + else if (email_auth) { + const email = req.body.email; + if (!email || !emailRegEx.test(email)) return res.error(400, "E-mail is not valid"); + if (await UserModel.exists({ email })) return res.error(400, "E-mail is already in use"); + user.email = email; + user.email_code = await bcrypt.hash(`${Date.now()}-${Math.floor(Math.random() * 1e20)}`, 10) - user.password = await bcrypt.hash(password, await bcrypt.genSalt(10)); + transporter.sendMail({ + from: transporter.options.auth.user, + to: email, + subject: name + ", please verify your email", + html: ` +

Verify your email in ${forum_name}-forum

+ Click here to verify your email + ` + }, (err, info) => { + if (err) return res.error(500, "Failed to send email"); + }); + + } + + user.password = await bcrypt.hash(password, 10); await user.save(); req.session.userID = user.id; diff --git a/util/admin.js b/util/admin.js index 40ee083..5def012 100644 --- a/util/admin.js +++ b/util/admin.js @@ -6,7 +6,7 @@ mongoose.connect(process.env.MONGO_DB_URL, () => console.log("Database is connec const { UserModel } = require("../models"); (async () => { - const member= await UserModel.get("0"); + const member = await UserModel.get("0"); member.admin = true; console.log(await member.save()); })(); \ No newline at end of file diff --git a/views/register.ejs b/views/register.ejs index 9798e2e..78ffc84 100644 --- a/views/register.ejs +++ b/views/register.ejs @@ -15,6 +15,9 @@
+ <% if (mail) { %> + + <% } %>