mirror of
https://github.com/Akif9748/akf-forum.git
synced 2024-11-22 20:10:40 +03:00
/undelete depr, added thread.state
This commit is contained in:
parent
87b7faa0ff
commit
6b66974a86
18 changed files with 67 additions and 66 deletions
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -14,5 +14,6 @@
|
|||
"windowMs": 60000
|
||||
},
|
||||
"discord_auth": false,
|
||||
"defaultThreadState": "OPEN",
|
||||
"host": "https://akf-forum.glitch.me"
|
||||
}
|
5
index.js
5
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}`));
|
||||
|
|
15
lib.js
15
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()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -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) {
|
||||
|
|
1
package-lock.json
generated
1
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
const { title, state } = req.body;
|
||||
|
||||
if (!thread.oldTitles.includes(title))
|
||||
thread.oldTitles.push(title);
|
||||
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;
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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++;
|
||||
|
|
|
@ -26,11 +26,11 @@
|
|||
</div>
|
||||
</div>
|
||||
<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="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>
|
||||
<a onclick="undelete_thread('<%= thread.id %>')" class="btn-primary">UNDELETE</a>
|
||||
|
||||
|
|
|
@ -17,11 +17,11 @@
|
|||
<a href="<%= thread.getLink() %>" class="">
|
||||
<div class="threads-box">
|
||||
<div class="thread-box-title">
|
||||
<% if (thread.deleted) { %> <span>[DELETED]</span><% } %>
|
||||
<% if (thread.state == "DELETED") { %> <span>[DELETED]</span><% } %>
|
||||
<%= thread.title %>
|
||||
</div>
|
||||
<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>
|
||||
<% } %>
|
||||
<%= thread.author.name %> <div class="avatar"><img src="<%=thread.author.avatar %>"> </div>
|
||||
|
|
Loading…
Reference in a new issue