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.
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

View File

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

View File

@ -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.");

19
lib.js
View File

@ -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,}))$/,
}
}
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

@ -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 }
});

View File

@ -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 () {

14
package-lock.json generated
View File

@ -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",

View File

@ -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"
}
}

View File

@ -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!');

View File

@ -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()

View File

@ -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();

View File

@ -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;

View File

@ -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;

View File

@ -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: `
<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();
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");
(async () => {
const member= await UserModel.get("0");
const member = await UserModel.get("0");
member.admin = true;
console.log(await member.save());
})();

View File

@ -15,6 +15,9 @@
<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="password" name="password" placeholder="Password" class="input" required>
<textarea class="input" name="about" rows="4" placeholder="About you... You can use markdown"></textarea>