diff --git a/APIDOCS.md b/APIDOCS.md index 764aa9e..59d46b3 100644 --- a/APIDOCS.md +++ b/APIDOCS.md @@ -55,7 +55,6 @@ You can change them in config.json. - GET `/:id` for fetch thread. - DELETE `/:id` for delete thread. - PATCH `/:id` for edit thread. -- POST `/:id/undelete` for undelete thread. - GET `/:id/messages?skip=0&limit=10` for fetch messages in thread. - POST `/` for create thread. diff --git a/README.md b/README.md index a46ab04..18ea482 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Akf-forum has got an API for AJAX (fetch), other clients etc. And, you can learn - email auth. - thread.state =="approval" for threads. - old contents / titles add to forum interface - +- limits ## Major Version History - V4: Caching - V3: New Theme diff --git a/config.json.example b/config.json.example index 1859127..1a9c90b 100644 --- a/config.json.example +++ b/config.json.example @@ -14,5 +14,6 @@ "windowMs": 60000 }, "discord_auth": false, + "defaultThreadState": "OPEN", "host": "https://akf-forum.glitch.me" } \ No newline at end of file diff --git a/index.js b/index.js index b725f61..3c5e645 100644 --- a/index.js +++ b/index.js @@ -7,7 +7,6 @@ const express = require('express'), fs = require("fs"), app = express(), - { urlencoded: BP } = require('body-parser'), { mw: IP } = require('request-ip'), { RL } = require('./lib'), SES = require('express-session'), @@ -23,7 +22,7 @@ app.ips = []; app.set("view engine", "ejs"); 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 }), async (req, res, next) => { 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) 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")) app.use("/" + file.replace(".js", ""), require(`./routes/${file}`)); diff --git a/lib.js b/lib.js index 1e567ba..34a3805 100644 --- a/lib.js +++ b/lib.js @@ -1,7 +1,12 @@ const RL = require('express-rate-limit'); -module.exports.RL = (windowMs = 60_000, max = 1) => - RL({ - windowMs, max, standardHeaders: true, legacyHeaders: false, - handler: (req, res, next, opts) => !req.user?.admin ? res.error(opts.statusCode, "You are begin ratelimited") : next() - }) +module.exports = { + threadEnum: ["OPEN", "APPROVAL", "DELETED"], + RL(windowMs = 60_000, max = 1) { + return RL({ + windowMs, max, standardHeaders: true, legacyHeaders: false, + handler: (req, res, next, opts) => !req.user?.admin ? res.error(opts.statusCode, "You are begin ratelimited") : next() + }) + } + +} \ No newline at end of file diff --git a/models/Thread.js b/models/Thread.js index 2d19d74..f760e30 100644 --- a/models/Thread.js +++ b/models/Thread.js @@ -1,8 +1,8 @@ const mongoose = require("mongoose"); const cache = require("./cache") const MessageModel = require("./Message"); -const { limits } = require("../config.json"); - +const { limits, defaultThreadState } = require("../config.json"); +const { threadEnum } = require("../lib"); const schema = new mongoose.Schema({ id: { type: String, unique: true }, @@ -14,9 +14,8 @@ const schema = new mongoose.Schema({ oldTitles: [String], time: { type: Date, default: Date.now }, - deleted: { type: Boolean, default: false }, edited: { type: Boolean, default: false }, - + state: { type: String, default: defaultThreadState, enum: threadEnum }, messages: [String], views: { type: Number, default: 0 } @@ -24,7 +23,7 @@ const schema = new mongoose.Schema({ 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" }; } schema.methods.messageCount = async function (admin = false) { diff --git a/package-lock.json b/package-lock.json index c403210..a612b38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "GPL-3.0-or-later", "dependencies": { "bcrypt": "^5.0.1", - "body-parser": "^1.19.2", "connect-mongo": "^4.6.0", "dotenv": "^16.0.1", "ejs": "^3.1.6", diff --git a/package.json b/package.json index 27743e2..8842b86 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "homepage": "https://akf-forum.glitch.me/", "dependencies": { "bcrypt": "^5.0.1", - "body-parser": "^1.19.2", "connect-mongo": "^4.6.0", "dotenv": "^16.0.1", "ejs": "^3.1.6", diff --git a/public/js/thread.js b/public/js/thread.js index b618a18..ad289cf 100644 --- a/public/js/thread.js +++ b/public/js/thread.js @@ -19,7 +19,7 @@ window.delete_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; alert(`Thread undeleted`); location.reload(); diff --git a/routes/api/routes/categories.js b/routes/api/routes/categories.js index ed85e65..19ed676 100644 --- a/routes/api/routes/categories.js +++ b/routes/api/routes/categories.js @@ -19,10 +19,7 @@ app.get("/", async (req, res) => { res.complate(categories); }); -app.get("/:id", async (req, res) => { - const { category } = req; - res.complate(category); -}); +app.get("/:id", async (req, res) => res.complate(req.category)); app.patch("/:id", async (req, res) => { const { category } = req; @@ -31,9 +28,7 @@ app.patch("/:id", async (req, res) => { res.complate(await category.save()); }); -app.delete("/:id", async (req, res) => { - res.complate(await CategoryModel.deleteOne({ id: req.params.id })); -}); +app.delete("/:id", async (req, res) => res.complate(await CategoryModel.deleteOne({ id: req.params.id }))); app.post("/", async (req, res) => { const { name, desp } = req.body; diff --git a/routes/api/routes/messages.js b/routes/api/routes/messages.js index ef984bf..2ba7d65 100644 --- a/routes/api/routes/messages.js +++ b/routes/api/routes/messages.js @@ -28,7 +28,7 @@ app.patch("/:id/", async (req, res) => { if (!content) return res.error(400, "Missing message content in request body."); 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; @@ -48,7 +48,7 @@ app.post("/", RL(), async (req, res) => { if (!content) return res.error(400, "Missing message content in request body."); 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); diff --git a/routes/api/routes/search.js b/routes/api/routes/search.js index 9918474..97fcb30 100644 --- a/routes/api/routes/search.js +++ b/routes/api/routes/search.js @@ -28,7 +28,9 @@ app.get("/messages", 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."); - 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.authorID) query.authorID = req.query.authorID; const results = await ThreadModel.find(query, null, req.so) diff --git a/routes/api/routes/threads.js b/routes/api/routes/threads.js index b6659b4..b122ba4 100644 --- a/routes/api/routes/threads.js +++ b/routes/api/routes/threads.js @@ -1,6 +1,6 @@ const { MessageModel, ThreadModel } = require("../../../models"); const { Router } = require("express") -const { RL } = require('../../../lib'); +const { RL, threadEnum } = require('../../../lib'); const app = Router(); 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.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}.`) 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."); 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 (content.length < 5 || content.length > limits.message) return res.error(400, "content must be between 5 - 1024 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 - ${limits.message} characters`); const { user } = req; const thread = await new ThreadModel({ title, author: user }).takeId() if (category) @@ -57,21 +57,36 @@ app.post("/", RL(5 * 60_000, 1), async (req, res) => { res.complate(thread); }); -app.patch("/:id/", async (req, res) => { +app.patch("/:id/", async (req, res) => { const { user, thread } = req; if (user.id !== thread.authorID && !user.admin) return res.error(403, "You have not got permission for this."); - const { title } = req.body; - if (!title) return res.error(400, "Missing thread title in request body."); - const limits = req.app.get("limits"); + if (!Object.values(req.body).some(Boolean)) return res.error(400, "Missing thread informations for update in request body."); - if (title.length < 5 || title.length > limits.title) return res.error(400, "title must be between 5 - 128 characters"); - thread.title = title; - - if (!thread.oldTitles.includes(title)) - thread.oldTitles.push(title); + const { title, state } = req.body; + + if (title) { + const limits = req.app.get("limits"); + + 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.oldTitles.push(thread.title = 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(); @@ -84,27 +99,13 @@ 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.deleted) return res.error(403, "This thread is already deleted."); - thread.deleted = true; + if (thread.state == "DELETED") return res.error(403, "This thread is already deleted."); + thread.state = "DELETED"; await thread.save(); await MessageModel.updateMany({ threadID: thread.id }, { deleted: true }); 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; \ No newline at end of file diff --git a/routes/api/routes/users.js b/routes/api/routes/users.js index d015d02..eb887bb 100644 --- a/routes/api/routes/users.js +++ b/routes/api/routes/users.js @@ -60,13 +60,13 @@ app.patch("/:id/", async (req, res) => { 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 }); member.name = name; } 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; } if (theme || ["default", "black"].includes(theme)) member.theme = theme; diff --git a/routes/search.js b/routes/search.js index b1d80bf..4391b2b 100644 --- a/routes/search.js +++ b/routes/search.js @@ -37,7 +37,9 @@ app.get("/messages", 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."); - 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.authorID) query.authorID = req.query.authorID; const threads = await ThreadModel.find(query, null, req.so) diff --git a/routes/threads.js b/routes/threads.js index 4f822d8..69d30d3 100644 --- a/routes/threads.js +++ b/routes/threads.js @@ -4,7 +4,7 @@ const { ThreadModel, MessageModel, CategoryModel } = require("../models") app.get("/", async (req, res) => { 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 }); 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 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.pages = Math.ceil(thread.count / 10); thread.views++; diff --git a/views/thread.ejs b/views/thread.ejs index e3ede68..995fa28 100644 --- a/views/thread.ejs +++ b/views/thread.ejs @@ -26,11 +26,11 @@
- <% if (user && !thread.deleted){ %> + <% if ((user.id === thread.authorID || user.admin ) && thread.state !== "DELETED"){ %> DELETE EDIT - <% } else if (thread.deleted) { %> + <% } else if (thread.state == "DELETED") { %>

This thread has been deleted

UNDELETE diff --git a/views/threads.ejs b/views/threads.ejs index 1e6418f..288619e 100644 --- a/views/threads.ejs +++ b/views/threads.ejs @@ -17,11 +17,11 @@
- <% if (thread.deleted) { %> [DELETED]<% } %> + <% if (thread.state == "DELETED") { %> [DELETED]<% } %> <%= thread.title %>