mirror of
https://github.com/Akif9748/akf-forum.git
synced 2024-11-22 12:00:41 +03:00
HTTP codes, approval system for user, email
This commit is contained in:
parent
32c2d3d1ca
commit
764dcc93f0
16 changed files with 101 additions and 26 deletions
12
README.md
12
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.
|
`"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
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
6
index.js
6
index.js
|
@ -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.");
|
||||||
|
|
19
lib.js
19
lib.js
|
@ -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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -12,13 +12,12 @@ const schema = new mongoose.Schema({
|
||||||
|
|
||||||
title: { type: String, maxlength: limits.title },
|
title: { type: String, maxlength: limits.title },
|
||||||
oldTitles: [String],
|
oldTitles: [String],
|
||||||
|
|
||||||
time: { type: Date, default: Date.now },
|
time: { type: Date, default: Date.now },
|
||||||
edited: { type: Boolean, default: false },
|
edited: { type: Boolean, default: false },
|
||||||
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 }
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
14
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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!');
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -22,7 +22,7 @@ app.get("/discord", async (req, res) => {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) return res.error(500, "Bad request to discord");
|
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.");
|
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;
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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());
|
||||||
})();
|
})();
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue