HTTP codes, approval system for user, email

This commit is contained in:
Akif9748 2022-09-24 01:39:06 +03:00
parent 32c2d3d1ca
commit 764dcc93f0
16 changed files with 101 additions and 26 deletions

View file

@ -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. `"discord_auth": "your_app_id"` in config.json.
Add your app secret to `.env` as `DISCORD_SECRET`. Add your app secret to `.env` as `DISCORD_SECRET`.
Create a redirect url in discord developer portal: 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 ## API
Akf-forum has got an API for AJAX (fetch), other clients etc. And, you can learn about API in `APIDOCS.md`. 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 - upload other photos, model for it
- categories page is need a update, thread count in category - categories page is need a update, thread count in category
- Disable last seen button for web. - Disable last seen button for web.
- email auth.
- old contents / titles add to forum interface - old contents / titles add to forum interface
- add ban button to user profile. - add ban button to user profile.
- change password. - change password.
- add approval threads page. - add approval threads page.
- who liked a message for web. - who liked a message for web.
- edit config from web admin panel. - edit config from web admin panel.
- user.state for ban, delete, etc.
- Add a feature list to README.md
## Major Version History ## Major Version History
- V4: Caching - V4: Caching
- V3: New Theme - V3: New Theme

View file

@ -15,5 +15,6 @@
}, },
"discord_auth": "", "discord_auth": "",
"defaultThreadState": "OPEN", "defaultThreadState": "OPEN",
"email_auth": false,
"host": "https://akf-forum.glitch.me" "host": "https://akf-forum.glitch.me"
} }

View file

@ -22,19 +22,21 @@ app.ips = [];
app.set("view engine", "ejs"); app.set("view engine", "ejs");
app.set("limits", limits); 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 }), SES({ secret: process.env.SECRET, store: MS.create({ clientPromise: DB, stringify: false }), resave: true, saveUninitialized: true }),
async (req, res, next) => { async (req, res, next) => {
if (app.ips.includes(req.clientIp)) return res.status(403).send("You are banned from this forum."); 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 }, { req.user = req.session.userID ? await UserModel.findOneAndUpdate({ id: req.session.userID }, {
lastSeen: Date.now(), $addToSet: { ips: req.clientIp } lastSeen: Date.now(), $addToSet: { ips: req.clientIp }
}): null; }) : null;
res.reply = (page, options = {}, status = 200) => res.status(status) res.reply = (page, options = {}, status = 200) => res.status(status)
.render(page, { user: req.user, theme: req.user?.theme || def_theme, forum_name, description, ...options }); .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); 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) { if (req.user?.deleted) {
req.session.destroy(); req.session.destroy();
return res.error(403, "Your account has been deleted."); return res.error(403, "Your account has been deleted.");

17
lib.js
View file

@ -1,5 +1,7 @@
const RL = require('express-rate-limit'); const RL = require('express-rate-limit');
const nodemailer = require("nodemailer");
const config = require("./config.json");
require("dotenv").config();
module.exports = { module.exports = {
threadEnum: ["OPEN", "APPROVAL", "DELETED"], threadEnum: ["OPEN", "APPROVAL", "DELETED"],
themes: ["default", "black"], themes: ["default", "black"],
@ -8,6 +10,17 @@ module.exports = {
windowMs, max, standardHeaders: true, legacyHeaders: false, windowMs, max, standardHeaders: true, legacyHeaders: false,
handler: (req, res, next, opts) => !req.user?.admin ? res.error(opts.statusCode, "You are begin ratelimited") : next() 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,}))$/,
} }
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
}
});

View file

@ -18,7 +18,6 @@ const schema = new mongoose.Schema({
state: { type: String, default: defaultThreadState, enum: threadEnum }, state: { type: String, default: defaultThreadState, enum: threadEnum },
messages: [String], messages: [String],
views: { type: Number, default: 0 } views: { type: Number, default: 0 }
}); });

View file

@ -1,5 +1,5 @@
const mongoose = require("mongoose") 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({ const schema = new mongoose.Schema({
id: { type: String, unique: true }, id: { type: String, unique: true },
discordID: { type: String }, discordID: { type: String },
@ -15,7 +15,10 @@ const schema = new mongoose.Schema({
hideLastSeen: { type: Boolean, default: false }, hideLastSeen: { type: Boolean, default: false },
ips: { type: [String], default: [], select: false }, ips: { type: [String], default: [], select: false },
password: { type: String, 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 () { schema.methods.takeId = async function () {

14
package-lock.json generated
View file

@ -19,6 +19,7 @@
"mongoose": "^6.6.1", "mongoose": "^6.6.1",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.7",
"nodemailer": "^6.7.8",
"request-ip": "^3.3.0" "request-ip": "^3.3.0"
}, },
"engines": { "engines": {
@ -1308,6 +1309,14 @@
"webidl-conversions": "^3.0.0" "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": { "node_modules/nopt": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "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": { "nopt": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",

View file

@ -35,6 +35,7 @@
"mongoose": "^6.6.1", "mongoose": "^6.6.1",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.7",
"nodemailer": "^6.7.8",
"request-ip": "^3.3.0" "request-ip": "^3.3.0"
} }
} }

View file

@ -20,6 +20,7 @@ app.use(async (req, res, next) => {
const user = await UserModel.findOne({ name }); 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 || 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!'); if (!await bcrypt.compare(password, user.password)) return res.error(401, 'Incorrect Password!');

View file

@ -25,7 +25,7 @@ app.delete("/:id/", async (req, res) => {
if (user.id != message.authorID && !user.admin) if (user.id != message.authorID && !user.admin)
return res.error(403, "You have not got permission for this."); 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; message.deleted = true;
await message.save() await message.save()

View file

@ -99,7 +99,7 @@ app.delete("/:id/", async (req, res) => {
if (user.id != thread.authorID && !user.admin) if (user.id != thread.authorID && !user.admin)
return res.error(403, "You have not got permission for this."); 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"; thread.state = "DELETED";
await thread.save(); await thread.save();

View file

@ -2,7 +2,7 @@ const { Router } = require("express")
const { UserModel } = require("../models"); const { UserModel } = require("../models");
const fetch = require("node-fetch"); const fetch = require("node-fetch");
const app = Router(); 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) => { app.get("/discord", async (req, res) => {
const client_id = discord_auth; const client_id = discord_auth;
@ -39,7 +39,7 @@ app.get("/discord", async (req, res) => {
return res.error(403, "Your forum account is already linked to a discord account."); return res.error(403, "Your forum account is already linked to a discord account.");
if (forum) 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.discordID = discord.id;
req.user.discord_code = code; req.user.discord_code = code;
@ -75,7 +75,7 @@ app.get("/discord", async (req, res) => {
}); });
app.delete("/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."); 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.discordID = undefined;
req.user.discord_code = 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."); 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; module.exports = app;

View file

@ -13,8 +13,8 @@ app.post("/", async (req, res) => {
if (!name || !password) return res.error(400, "You forgot entering some values") if (!name || !password) return res.error(400, "You forgot entering some values")
const member = await UserModel.findOne({ name }, "+password"); const member = await UserModel.findOne({ name }, "+password");
if (!member || member.deleted) return res.error(403, 'Incorrect username!'); if (!member || member.deleted) return res.error(401, 'Incorrect username!');
if (!await bcrypt.compare(password, member.password)) return res.error(403, 'Incorrect password!'); if (!await bcrypt.compare(password, member.password)) return res.error(401, 'Incorrect password!');
req.session.userID = member.id; req.session.userID = member.id;

View file

@ -1,10 +1,10 @@
const { UserModel } = require("../models"); const { UserModel } = require("../models");
const { Router } = require("express") const { Router } = require("express")
const bcrypt = require("bcrypt"); const bcrypt = require("bcrypt");
const { RL } = require('../lib'); const { RL, transporter, emailRegEx } = require('../lib');
const app = Router(); const app = Router();
const { email_auth, forum_name, host } = require("../config.json");
app.get("/", (req, res) => res.reply("register", { user: null, discord: req.app.get("discord_auth") })); 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) => { 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() 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: `
<h1>Verify your email in ${forum_name}-forum</h1>
<a href="${host}/auth/email?code=${user.email_code}">Click here to verify your email</a>
`
}, (err, info) => {
if (err) return res.error(500, "Failed to send email");
});
}
user.password = await bcrypt.hash(password, 10);
await user.save(); await user.save();
req.session.userID = user.id; req.session.userID = user.id;

View file

@ -6,7 +6,7 @@ mongoose.connect(process.env.MONGO_DB_URL, () => console.log("Database is connec
const { UserModel } = require("../models"); const { UserModel } = require("../models");
(async () => { (async () => {
const member= await UserModel.get("0"); const member = await UserModel.get("0");
member.admin = true; member.admin = true;
console.log(await member.save()); console.log(await member.save());
})(); })();

View file

@ -15,6 +15,9 @@
<form action="/register" method="post"> <form action="/register" method="post">
<% if (mail) { %>
<input type="email" name="email" placeholder="Email" class="input" required>
<% } %>
<input type="text" name="name" placeholder="Username" class="input" required> <input type="text" name="name" placeholder="Username" class="input" required>
<input type="password" name="password" placeholder="Password" class="input" required> <input type="password" name="password" placeholder="Password" class="input" required>
<textarea class="input" name="about" rows="4" placeholder="About you... You can use markdown"></textarea> <textarea class="input" name="about" rows="4" placeholder="About you... You can use markdown"></textarea>