/undelete depr, added thread.state

This commit is contained in:
Akif9748 2022-09-21 23:54:48 +03:00
parent 87b7faa0ff
commit 6b66974a86
18 changed files with 67 additions and 66 deletions

View file

@ -55,7 +55,6 @@ You can change them in config.json.
- GET `/:id` for fetch thread. - GET `/:id` for fetch thread.
- DELETE `/:id` for delete thread. - DELETE `/:id` for delete thread.
- PATCH `/:id` for edit thread. - PATCH `/:id` for edit thread.
- POST `/:id/undelete` for undelete thread.
- GET `/:id/messages?skip=0&limit=10` for fetch messages in thread. - GET `/:id/messages?skip=0&limit=10` for fetch messages in thread.
- POST `/` for create thread. - POST `/` for create thread.

View file

@ -53,7 +53,7 @@ Akf-forum has got an API for AJAX (fetch), other clients etc. And, you can learn
- email auth. - email auth.
- thread.state =="approval" for threads. - thread.state =="approval" for threads.
- old contents / titles add to forum interface - old contents / titles add to forum interface
- limits
## Major Version History ## Major Version History
- V4: Caching - V4: Caching
- V3: New Theme - V3: New Theme

View file

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

View file

@ -7,7 +7,6 @@ const
express = require('express'), express = require('express'),
fs = require("fs"), fs = require("fs"),
app = express(), app = express(),
{ urlencoded: BP } = require('body-parser'),
{ mw: IP } = require('request-ip'), { mw: IP } = require('request-ip'),
{ RL } = require('./lib'), { RL } = require('./lib'),
SES = require('express-session'), SES = require('express-session'),
@ -23,7 +22,7 @@ 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(), IP(), BP({ extended: true }), 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.");
@ -47,7 +46,7 @@ app.use(express.static("public"), express.json(), IP(), BP({ extended: true }),
if (discord_auth) if (discord_auth)
app.set("discord_auth", `https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_CLIENT}&redirect_uri=${host}%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=${host}%2Fdiscord_auth%2Fhash&response_type=token&scope=identify`);
if (RLS.enabled) app.use(RL(RSL.windowMs, RLS.max)); if (RLS.enabled) app.use(RL(RLS.windowMs, RLS.max));
for (const file of fs.readdirSync("./routes")) for (const file of fs.readdirSync("./routes"))
app.use("/" + file.replace(".js", ""), require(`./routes/${file}`)); app.use("/" + file.replace(".js", ""), require(`./routes/${file}`));

9
lib.js
View file

@ -1,7 +1,12 @@
const RL = require('express-rate-limit'); const RL = require('express-rate-limit');
module.exports.RL = (windowMs = 60_000, max = 1) => module.exports = {
RL({ threadEnum: ["OPEN", "APPROVAL", "DELETED"],
RL(windowMs = 60_000, max = 1) {
return RL({
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()
}) })
}
}

View file

@ -1,8 +1,8 @@
const mongoose = require("mongoose"); const mongoose = require("mongoose");
const cache = require("./cache") const cache = require("./cache")
const MessageModel = require("./Message"); const MessageModel = require("./Message");
const { limits } = require("../config.json"); const { limits, defaultThreadState } = require("../config.json");
const { threadEnum } = require("../lib");
const schema = new mongoose.Schema({ const schema = new mongoose.Schema({
id: { type: String, unique: true }, id: { type: String, unique: true },
@ -14,9 +14,8 @@ const schema = new mongoose.Schema({
oldTitles: [String], oldTitles: [String],
time: { type: Date, default: Date.now }, time: { type: Date, default: Date.now },
deleted: { type: Boolean, default: false },
edited: { type: Boolean, default: false }, edited: { type: Boolean, default: false },
state: { type: String, default: defaultThreadState, enum: threadEnum },
messages: [String], messages: [String],
views: { type: Number, default: 0 } views: { type: Number, default: 0 }
@ -24,7 +23,7 @@ const schema = new mongoose.Schema({
schema.methods.get_author = cache.getAuthor; schema.methods.get_author = cache.getAuthor;
schema.methods.get_category = () => async function () { schema.methods.get_category = async function () {
return await require("./Category").findOne({ id: this.categoryID }) || { id: this.categoryID, name: "Unknown" }; return await require("./Category").findOne({ id: this.categoryID }) || { id: this.categoryID, name: "Unknown" };
} }
schema.methods.messageCount = async function (admin = false) { schema.methods.messageCount = async function (admin = false) {

1
package-lock.json generated
View file

@ -10,7 +10,6 @@
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"dependencies": { "dependencies": {
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"body-parser": "^1.19.2",
"connect-mongo": "^4.6.0", "connect-mongo": "^4.6.0",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"ejs": "^3.1.6", "ejs": "^3.1.6",

View file

@ -26,7 +26,6 @@
"homepage": "https://akf-forum.glitch.me/", "homepage": "https://akf-forum.glitch.me/",
"dependencies": { "dependencies": {
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"body-parser": "^1.19.2",
"connect-mongo": "^4.6.0", "connect-mongo": "^4.6.0",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"ejs": "^3.1.6", "ejs": "^3.1.6",

View file

@ -19,7 +19,7 @@ window.delete_thread = async function (id) {
} }
window.undelete_thread = async function (id) { window.undelete_thread = async function (id) {
const res = await request(`/api/threads/${id}/undelete`); const res = await request(`/api/threads/${id}/`, "PATCH", { state: "OPEN" });
if (res.error) return; if (res.error) return;
alert(`Thread undeleted`); alert(`Thread undeleted`);
location.reload(); location.reload();

View file

@ -19,10 +19,7 @@ app.get("/", async (req, res) => {
res.complate(categories); res.complate(categories);
}); });
app.get("/:id", async (req, res) => { app.get("/:id", async (req, res) => res.complate(req.category));
const { category } = req;
res.complate(category);
});
app.patch("/:id", async (req, res) => { app.patch("/:id", async (req, res) => {
const { category } = req; const { category } = req;
@ -31,9 +28,7 @@ app.patch("/:id", async (req, res) => {
res.complate(await category.save()); res.complate(await category.save());
}); });
app.delete("/:id", async (req, res) => { app.delete("/:id", async (req, res) => res.complate(await CategoryModel.deleteOne({ id: req.params.id })));
res.complate(await CategoryModel.deleteOne({ id: req.params.id }));
});
app.post("/", async (req, res) => { app.post("/", async (req, res) => {
const { name, desp } = req.body; const { name, desp } = req.body;

View file

@ -28,7 +28,7 @@ app.patch("/:id/", async (req, res) => {
if (!content) return res.error(400, "Missing message content in request body."); if (!content) return res.error(400, "Missing message content in request body.");
const limits = req.app.get("limits"); const limits = req.app.get("limits");
if (content.length < 5 || content.length > limits.message) return res.error(400, "content must be between 5 - 1024 characters"); if (content.length < 5 || content.length > limits.message) return res.error(400, `content must be between 5 - ${limits.message} characters`);
message.content = content; message.content = content;
@ -48,7 +48,7 @@ app.post("/", RL(), async (req, res) => {
if (!content) return res.error(400, "Missing message content in request body."); if (!content) return res.error(400, "Missing message content in request body.");
const limits = req.app.get("limits"); const limits = req.app.get("limits");
if (content.length < 5 || content.length > limits.message) return res.error(400, "content must be between 5 - 1024 characters"); if (content.length < 5 || content.length > limits.message) return res.error(400, `content must be between ${limits.message} characters`);
const thread = await ThreadModel.get(threadID); const thread = await ThreadModel.get(threadID);

View file

@ -28,7 +28,9 @@ app.get("/messages", async (req, res) => {
}); });
app.get("/threads", async (req, res) => { app.get("/threads", async (req, res) => {
if (!Object.values(req.query).length) return res.error(400, "Missing query parameters in request body."); if (!Object.values(req.query).length) return res.error(400, "Missing query parameters in request body.");
const query = { ...req.sq }; const query = {};
if (!req.user.admin) query.state = "OPEN";
if (req.query.q) query.title = { $regex: req.query.q, $options: "i" }; if (req.query.q) query.title = { $regex: req.query.q, $options: "i" };
if (req.query.authorID) query.authorID = req.query.authorID; if (req.query.authorID) query.authorID = req.query.authorID;
const results = await ThreadModel.find(query, null, req.so) const results = await ThreadModel.find(query, null, req.so)

View file

@ -1,6 +1,6 @@
const { MessageModel, ThreadModel } = require("../../../models"); const { MessageModel, ThreadModel } = require("../../../models");
const { Router } = require("express") const { Router } = require("express")
const { RL } = require('../../../lib'); const { RL, threadEnum } = require('../../../lib');
const app = Router(); const app = Router();
app.param("id", async (req, res, next, id) => { app.param("id", async (req, res, next, id) => {
@ -8,7 +8,7 @@ app.param("id", async (req, res, next, id) => {
if (!req.thread) return res.error(404, `We don't have any thread with id ${id}.`); if (!req.thread) return res.error(404, `We don't have any thread with id ${id}.`);
if (req.thread.deleted && !req.user?.admin) if (req.thread.state !== "OPEN" && !req.user?.admin)
return res.error(404, `You do not have permissions to view this thread with id ${id}.`) return res.error(404, `You do not have permissions to view this thread with id ${id}.`)
next(); next();
@ -44,8 +44,8 @@ app.post("/", RL(5 * 60_000, 1), async (req, res) => {
if (!content || !title) return res.error(400, "Missing content/title in request body."); if (!content || !title) return res.error(400, "Missing content/title in request body.");
const limits = req.app.get("limits"); const limits = req.app.get("limits");
if (title.length < 5 || title.length > limits.title) return res.error(400, "title must be between 5 - 128 characters"); if (title.length < 5 || title.length > limits.title) return res.error(400, `title must be between 5 - ${limits.title} characters`);
if (content.length < 5 || content.length > limits.message) return res.error(400, "content must be between 5 - 1024 characters"); if (content.length < 5 || content.length > limits.message) return res.error(400, `content must be between 5 - ${limits.message} characters`);
const { user } = req; const { user } = req;
const thread = await new ThreadModel({ title, author: user }).takeId() const thread = await new ThreadModel({ title, author: user }).takeId()
if (category) if (category)
@ -57,21 +57,36 @@ app.post("/", RL(5 * 60_000, 1), async (req, res) => {
res.complate(thread); res.complate(thread);
}); });
app.patch("/:id/", async (req, res) => {
app.patch("/:id/", async (req, res) => {
const { user, thread } = req; const { user, thread } = req;
if (user.id !== thread.authorID && !user.admin) return res.error(403, "You have not got permission for this."); if (user.id !== thread.authorID && !user.admin) return res.error(403, "You have not got permission for this.");
const { title } = req.body; if (!Object.values(req.body).some(Boolean)) return res.error(400, "Missing thread informations for update in request body.");
if (!title) return res.error(400, "Missing thread title in request body.");
const { title, state } = req.body;
if (title) {
const limits = req.app.get("limits"); const limits = req.app.get("limits");
if (title.length < 5 || title.length > limits.title) return res.error(400, "title must be between 5 - 128 characters"); if (title.length < 5 || title.length > limits.title) return res.error(400, `title must be between 5 - ${limits.title} characters`);
if (thread.oldTitles.at(-1) == title) return res.error(400, "You can't use the same title as the previous one.");
thread.title = title; thread.oldTitles.push(thread.title = title);
}
if (!thread.oldTitles.includes(title))
thread.oldTitles.push(title); if (state) {
if (!user.admin)
return res.error(403, "You have not got permission for change state.");
if (thread.state === state) return res.error(400, "You can't change thread state to same state.");
if (!threadEnum.includes(state)) return res.error(400, "Invalid thread state.");
if (thread.state === "DELETED")
await MessageModel.updateMany({ threadID: thread.id }, { deleted: false });
thread.state = state;
}
await thread.save(); await thread.save();
@ -84,27 +99,13 @@ 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.deleted) return res.error(403, "This thread is already deleted."); if (thread.state == "DELETED") return res.error(403, "This thread is already deleted.");
thread.deleted = true; thread.state = "DELETED";
await thread.save(); await thread.save();
await MessageModel.updateMany({ threadID: thread.id }, { deleted: true }); await MessageModel.updateMany({ threadID: thread.id }, { deleted: true });
res.complate(thread); res.complate(thread);
}) })
app.post("/:id/undelete", async (req, res) => {
const { thread } = req;
if (!thread.deleted) return res.error(404, "This thread is not deleted, first, delete it.");
thread.deleted = false;
thread.edited = true;
await thread.save();
await MessageModel.updateMany({ threadID: thread.id }, { deleted: false });
res.complate(thread);
})
module.exports = app; module.exports = app;

View file

@ -60,13 +60,13 @@ app.patch("/:id/", async (req, res) => {
if (name) { if (name) {
if (name.length < 3 || names > 25) return res.error(400, "Username must be between 3 - 25 characters"); if (name.length < 3 || names > 25) return res.error(400, `Username must be between 3 - ${names} characters`);
await SecretModel.updateOne({ id: member.id }, { username: name }); await SecretModel.updateOne({ id: member.id }, { username: name });
member.name = name; member.name = name;
} }
if (about) { if (about) {
if (about.length > desp) return res.error(400, "About must be under 256 characters"); if (about.length > desp) return res.error(400, `About must be under ${desp} characters`);
member.about = about; member.about = about;
} }
if (theme || ["default", "black"].includes(theme)) member.theme = theme; if (theme || ["default", "black"].includes(theme)) member.theme = theme;

View file

@ -37,7 +37,9 @@ app.get("/messages", async (req, res) => {
app.get("/threads", async (req, res) => { app.get("/threads", async (req, res) => {
if (!Object.values(req.query).length) return res.error(400, "Missing query parameters in request body."); if (!Object.values(req.query).length) return res.error(400, "Missing query parameters in request body.");
const query = { ...req.sq }; const query = {};
if (!req.user?.admin) query.state = "OPEN";
if (req.query.q) query.title = { $regex: req.query.q, $options: "i" }; if (req.query.q) query.title = { $regex: req.query.q, $options: "i" };
if (req.query.authorID) query.authorID = req.query.authorID; if (req.query.authorID) query.authorID = req.query.authorID;
const threads = await ThreadModel.find(query, null, req.so) const threads = await ThreadModel.find(query, null, req.so)

View file

@ -4,7 +4,7 @@ const { ThreadModel, MessageModel, CategoryModel } = require("../models")
app.get("/", async (req, res) => { app.get("/", async (req, res) => {
const page = Number(req.query.page) || 0; const page = Number(req.query.page) || 0;
const query = req.user?.admin ? {} : { deleted: false }; const query = req.user?.admin ? {} : { state: "OPEN" };
let threads = await ThreadModel.find(query).limit(10).skip(page * 10).sort({ time: -1 }); let threads = await ThreadModel.find(query).limit(10).skip(page * 10).sort({ time: -1 });
threads = await Promise.all(threads.map(thread => thread.get_author())); threads = await Promise.all(threads.map(thread => thread.get_author()));
@ -21,7 +21,7 @@ app.get("/:id/", async (req, res) => {
const page = Number(req.query.page || 0); const page = Number(req.query.page || 0);
const thread = await ThreadModel.get(id) const thread = await ThreadModel.get(id)
if (thread && (user?.admin || !thread.deleted)) { if (thread && (user?.admin || thread.state == "OPEN")) {
thread.count = await thread.messageCount(user?.admin); thread.count = await thread.messageCount(user?.admin);
thread.pages = Math.ceil(thread.count / 10); thread.pages = Math.ceil(thread.count / 10);
thread.views++; thread.views++;

View file

@ -26,11 +26,11 @@
</div> </div>
</div> </div>
<div style="text-align:center;padding:8px"> <div style="text-align:center;padding:8px">
<% if (user && !thread.deleted){ %> <% if ((user.id === thread.authorID || user.admin ) && thread.state !== "DELETED"){ %>
<a onclick="delete_thread('<%= thread.id %>')" class="btn-outline-primary">DELETE</a> <a onclick="delete_thread('<%= thread.id %>')" class="btn-outline-primary">DELETE</a>
<a onclick="edit_thread('<%= thread.id %>')" class="btn-outline-primary">EDIT</a> <a onclick="edit_thread('<%= thread.id %>')" class="btn-outline-primary">EDIT</a>
<% } else if (thread.deleted) { %> <% } else if (thread.state == "DELETED") { %>
<h3 style="display:inline;">This thread has been deleted</h3> <h3 style="display:inline;">This thread has been deleted</h3>
<a onclick="undelete_thread('<%= thread.id %>')" class="btn-primary">UNDELETE</a> <a onclick="undelete_thread('<%= thread.id %>')" class="btn-primary">UNDELETE</a>

View file

@ -17,11 +17,11 @@
<a href="<%= thread.getLink() %>" class=""> <a href="<%= thread.getLink() %>" class="">
<div class="threads-box"> <div class="threads-box">
<div class="thread-box-title"> <div class="thread-box-title">
<% if (thread.deleted) { %> <span>[DELETED]</span><% } %> <% if (thread.state == "DELETED") { %> <span>[DELETED]</span><% } %>
<%= thread.title %> <%= thread.title %>
</div> </div>
<div class="box-username"> <div class="box-username">
<% if (user?.admin && !thread.deleted){ %> <% if (user?.admin && thread.state !== "DELETED"){ %>
<a class="btn-danger" onclick="fetch('/api/threads/<%= thread.id %>/',{method:'DELETE'})"><i class="bx bx-trash bx-sm"></i></a> <a class="btn-danger" onclick="fetch('/api/threads/<%= thread.id %>/',{method:'DELETE'})"><i class="bx bx-trash bx-sm"></i></a>
<% } %> <% } %>
<%= thread.author.name %> <div class="avatar"><img src="<%=thread.author.avatar %>"> </div> <%= thread.author.name %> <div class="avatar"><img src="<%=thread.author.avatar %>"> </div>