diff --git a/APIDOCS.md b/APIDOCS.md index fbf3acb..1df62d1 100644 --- a/APIDOCS.md +++ b/APIDOCS.md @@ -18,28 +18,28 @@ But in front end, the API will works with session. ### Request types: - GET `/api/bans/` fetch all bans. - GET `/api/bans/:id` fetch a ban. -- POST `/api/bans/:id?reason=flood` for ban an IP adress. - DELETE `/api/bans/:id` for unban an IP adress. +- POST `/api/bans?reason=flood` for ban an IP adress. - GET `/api/users/:id` for fetch user. - DELETE `/api/users/:id/` for delete user. +- PATCH `/api/users/:id/` for edit user. - POST `/api/users/:id/undelete` for undelete user. - POST `/api/users/:id/admin` for give admin permissions for a user. -- PATCH `/api/users/:id/` for edit user. - GET `/api/threads/:id` for fetch thread. -- GET `/api/threads/:id/messages/` for fetch messages in thread. -- POST `/api/threads` for create thread. - DELETE `/api/threads/:id/` for delete thread. -- POST `/api/threads/:id/undelete` for undelete thread. - PATCH `/api/threads/:id/` for edit thread. +- POST `/api/threads/:id/undelete` for undelete thread. +- GET `/api/threads/:id/messages?skip=0&limit=10` for fetch messages in thread. +- POST `/api/threads` for create thread. - GET `/api/messages/:id` for fetch message. -- POST `/api/messages` for create message. - DELETE `/api/messages/:id/` for delete message. +- PATCH `/api/messages/:id/` for edit message. - POST `/api/messages/:id/undelete` for undelete message. - POST `/api/messages/:id/react/:type` for react to a message. -- PATCH `/api/messages/:id/` for edit message. +- POST `/api/messages` for create message. ### Example request: GET ```/api/messages/0``` @@ -67,7 +67,7 @@ GET ```/api/messages/0``` "__v": 0, "react": { "like": [0], - "dislike":[] + "dislike": [] }, "authorID": "0" } diff --git a/README.md b/README.md index 5b132dc..f7794a7 100644 --- a/README.md +++ b/README.md @@ -29,74 +29,41 @@ Akf-forum has got an API for AJAX, other clients etc. And, you can learn about A ## Roadmap ### TO-DO: -- If thread deleted, not show its messages in API. +| To do | Is done? | Priority | +| ----- | -------- | -------- | +| Profile Message | πŸ”΄ | LOW | +| from form to AJAX | 🟒 | HIGH | +| auto-scroll | 🟑 | LOW | +| Page support, support message limit correct | 🟒 | MEDIUM | +| Multi-theme support, black theme | 🟑 | LOW | +| Search | πŸ”΄ | MEDIUM | +| Footer | πŸ”΄ | LOW | +- If thread deleted, not show its messages in API. ? - Profile photos will store in database - replacer function global - author name of thread -- page for threads - users [] -- API, ?fast= +- page for threads - users - extra ratelimits - better edits -- IP BAN CLI IN ADMIN PANEL - -### Frontend -#### User -| To do | Is done? | Priority | -| ----- | -------- | -------- | -| Login via redirect query | 🟒 | HIGH | -| Register | 🟒 | HIGH | -| Logout | 🟒 | HIGH | -| Admin | 🟒 | HIGH | -| Message count | 🟒 | MEDIUM | -| Delete user | 🟒 | HIGH | -| Undelete | 🟒 | MEDIUM | -| About me | 🟒 | LOW | -| Edit user | 🟒 | HIGH | -| IP ban | 🟒 | MEDIUM | -| Profile Message | πŸ”΄ | MEDIUM | - -#### Messages -| To do | Is done? | Priority | -| ----- | -------- | -------- | -| Ratelimit | 🟒 | HIGH | -| Send | 🟒 | HIGH | -| Delete | 🟒 | HIGH | -| Regex for scripts | 🟒 | HIGH | -| Undelete | 🟒 | MEDIUM | -| React | 🟒 | MEDIUM | -| Edit | 🟒 | MEDIUM | - -#### Threads -| To do | Is done? | Priority | -| ----- | -------- | -------- | -| Ratelimit | 🟒 | HIGH | -| Create | 🟒 | HIGH | -| Delete | 🟒 | HIGH | -| Undelete | 🟒 | MEDIUM | -| Edit | 🟒 | MEDIUM | +- IP BAN fix +- APIDOCS query +- app.param for users in API +- message counts for API +- ZATEN SΔ°LΔ°NDΔ° BU KİŞİ & MESAJ ### API | To do | Is done? | ----- | -------- | RATELIMITS | 🟒 -| Get message**s** | 🟒 +| Get a lots of message & thread & user | πŸ”΄ | Create message & thread & user | 🟒 | Get message & thread & user | 🟒 | Delete message & thread & user | 🟒 | Undelete message & thread & user | 🟒 | Edit message & thread & user | 🟒 -### Other -| To do | Is done? | Priority | -| ----- | -------- | -------- | -| from form to AJAX | 🟒 | HIGH | -| auto-scroll | 🟒 | LOW | -| Page support, support message limit correct | 🟒 | MEDIUM | -| Multi-theme support, black theme | 🟑 | LOW | -| Search | πŸ”΄ | MEDIUM | -| Footer | πŸ”΄ | LOW | - ## Major Version History +- V4: Caching - V3: New Theme - V2: Backend fix, mongoose is fixed. Really big fix. - V1: Mongoose added. diff --git a/index.js b/index.js index 8bd4c91..e3dc9b2 100644 --- a/index.js +++ b/index.js @@ -1,18 +1,19 @@ -const { def_theme } = require("./config.json"), +const { UserModel, BanModel } = require("./models"), + { def_theme } = require("./config.json"), ipBlock = require('express-ip-block'), session = require('express-session'), - { UserModel, BanModel } = require("./models"), bodyParser = require('body-parser'), port = process.env.PORT || 3000, mongoose = require("mongoose"), express = require('express'), fs = require("fs"), app = express(); + app.ips = []; require("dotenv").config(); mongoose.connect(process.env.MONGO_DB_URL, - async () => console.log("Connected to mongoDB with", app.ips = await BanModel.find({}).select("ip"), "banned IPs")); + async () => console.log("Database is connected with", (app.ips = await BanModel.find({})).length, "banned IPs")); app.set("view engine", "ejs"); @@ -20,7 +21,7 @@ app.use(session({ secret: 'secret', resave: true, saveUninitialized: true }), bodyParser.urlencoded({ extended: true }), express.static("public"), express.json(), ipBlock(app.ips), async (req, res, next) => { - req.user = await UserModel.get(req.session.userid); + req.user = await UserModel.get(req.session.userID); res.reply = (page, options = {}, status = 200) => res.status(status) .render(page, { user: req.user, theme: req.user?.theme || def_theme, ...options }); diff --git a/models/Message.js b/models/Message.js index 3d562c5..691bd77 100644 --- a/models/Message.js +++ b/models/Message.js @@ -1,12 +1,10 @@ const mongoose = require("mongoose") -const UserModel = require("./User"); - +const cache = require("./cache") const schema = new mongoose.Schema({ id: { type: String, unique: true }, - + author: Object, threadID: String, - author: UserModel.schema, // user-model - + authorID: String, content: String, time: { type: Date, default: Date.now }, deleted: { type: Boolean, default: false }, @@ -17,7 +15,7 @@ const schema = new mongoose.Schema({ } }) -schema.virtual('authorID').get(function () { return this.author?.id; }); +schema.methods.get_author = cache.getAuthor schema.methods.takeId = async function () { this.id = String(await model.count() || 0); @@ -30,7 +28,9 @@ schema.methods.getLink = function (id = this.id) { const model = mongoose.model('message', schema); -model.get = id => model.findOne({ id }); - -module.exports = model; +model.get = async id => { + const message = await model.findOne({ id }) + return await message.get_author(); +}; +module.exports = model; \ No newline at end of file diff --git a/models/Thread.js b/models/Thread.js index 43e86b8..08a9c43 100644 --- a/models/Thread.js +++ b/models/Thread.js @@ -1,10 +1,11 @@ const mongoose = require("mongoose"); -const UserModel = require("./User"); +const cache = require("./cache") const MessageModel = require("./Message"); const schema = new mongoose.Schema({ id: { type: String, unique: true }, - author: UserModel.schema, + authorID: String, + author: Object, title: String, time: { type: Date, default: Date.now }, @@ -16,18 +17,20 @@ const schema = new mongoose.Schema({ }); -schema.virtual('authorID').get(function () { return this.author?.id; }); + +schema.methods.get_author = cache.getAuthor; + schema.methods.messageCount = async function (admin = false) { const query = { threadID: this.id }; if (!admin) query.deleted = false; return await MessageModel.count(query) || 0; }; + schema.methods.push = function (messageID) { this.messages.push(messageID); return this; } - schema.methods.takeId = async function () { this.id = await model.count() || 0; return this; @@ -39,6 +42,9 @@ schema.methods.getLink = function (id = this.id) { const model = mongoose.model('thread', schema); -model.get = id => model.findOne({ id }); +model.get = async id => { + const thread = await model.findOne({ id }) + return await thread.get_author(); +}; module.exports = model; \ No newline at end of file diff --git a/models/User.js b/models/User.js index f4da18a..3232635 100644 --- a/models/User.js +++ b/models/User.js @@ -14,7 +14,6 @@ const schema = new mongoose.Schema({ }); - schema.methods.takeId = async function () { this.id = String(await model.count() || 0); return this; diff --git a/models/cache.js b/models/cache.js new file mode 100644 index 0000000..9fd4ffa --- /dev/null +++ b/models/cache.js @@ -0,0 +1,14 @@ +const UserModel = require("./User"); +const UserCache = []; + +module.exports.getAuthor = async function () { + console.log("User Cache Length:", UserCache.length); + const id = this.authorID || this.author?.id; + let user = UserCache.find(user => user?.id == id) + if (!user) { + user = await UserModel.findOne({ id }) + UserCache.push(user) + } + this.author = user; + return this; +} \ No newline at end of file diff --git a/routes/.js b/routes/.js index 1c96c6e..464e1b1 100644 --- a/routes/.js +++ b/routes/.js @@ -9,8 +9,7 @@ app.get("/", async (req, res) => { mem = process.memoryUsage().heapUsed / Math.pow(2, 20), users = await UserModel.count({deleted:false}), threads = await ThreadModel.count({deleted:false}), - messages = await MessageModel.count({deleted:false}), - user = req.user; + messages = await MessageModel.count({deleted:false}); res.reply("index", { mem, users, threads, messages }) diff --git a/routes/api/routes/messages.js b/routes/api/routes/messages.js index cad5705..027fcfd 100644 --- a/routes/api/routes/messages.js +++ b/routes/api/routes/messages.js @@ -5,43 +5,47 @@ const { Router } = require("express") const app = Router(); +app.param("id", async (req, res, next, id) => { + req.message = await ThreadModel.get(id); + + if (!req.message) return res.error(404, `We don't have any message with id ${id}.`); + + if(req.message.deleted && !req.user?.admin) + return res.error(404, `You do not have permissions to view this message with id ${id}.`) + + next(); +}); + app.get("/:id", async (req, res) => { - - const message = await MessageModel.get(req.params.id); - - if (!message || (message.deleted && req.user && !req.user.admin)) return res.error(404, `We don't have any message with id ${req.params.id}.`); - - res.complate(message.toObject({ virtuals: true })); + + res.complate(message); }) app.patch("/:id/", async (req, res) => { + - const message = await MessageModel.get(req.params.id); + const { message, user } = req; - if (!message || (message.deleted && req.user && !req.user.admin)) return res.error(404, `We don't have any message with id ${req.params.id}.`); - - if (req.user.id !== message.authorID && !req.user.admin) return res.error(403, "You have not got permission for this."); + if (user.id !== message.authorID && !user.admin) return res.error(403, "You have not got permission for this."); const { content = null } = req.body; if (!content) return res.error(400, "Missing message content in request body."); message.content = content; - message.edited=true; + message.edited = true; await message.save(); - res.complate(message.toObject({ virtuals: true })); + res.complate(message); }) app.post("/", rateLimit({ windowMs: 60_000, max: 1, standardHeaders: true, legacyHeaders: false, handler: (request, response, next, options) => - !request.user.admin ? - response.error(options.statusCode, "You are begin ratelimited") - : next() + !request.user.admin ? response.error(options.statusCode, "You are begin ratelimited") : next() }), async (req, res) => { - const { threadID = null, content = null } = req.body; + const { threadID, content } = req.body; if (!content) return res.error(400, "Missing message content in request body."); const thread = await ThreadModel.get(threadID); @@ -52,13 +56,13 @@ app.post("/", rateLimit({ await message.save(); await thread.push(message.id).save(); - res.complate(message.toObject({ virtuals: true })); + res.complate(message); }) app.post("/:id/react/:type", async (req, res) => { + - const message = await MessageModel.get(req.params.id); - if (!message) return error(res, 404, `We don't have any message with id ${req.params.id}.`); + const { message } = req; if (req.params.type == "like") { if (message.react.like.includes(req.user.id)) @@ -78,43 +82,41 @@ app.post("/:id/react/:type", async (req, res) => { message.react.like.pull(req.user.id); } - } else { + } else return res.error(400, `We don't have any react type with name ${req.params.type}.`); - } await message.save(); - res.complate(message.toObject({ virtuals: true })); + res.complate(message); }); app.delete("/:id/", async (req, res) => { - const message = await MessageModel.get(req.params.id); - if (!message || (message.deleted && req.user && !req.user.admin)) - return res.error(404, `We don't have any message with id ${req.params.id}.`); - const user = req.user; + + + const { message, user } = req; + if (user.id != message.authorID && !user.admin) return res.error(403, "You have not got permission for this."); + message.deleted = true; await message.save(); - res.complate(message.toObject({ virtuals: true })); + res.complate(message); }) app.post("/:id/undelete", async (req, res) => { - if (!req.user.admin) return res.error(403, "You have not got permission for this."); + - const message = await MessageModel.get(req.params.id); - - if (!message) return res.error(404, `We don't have any message with id ${req.params.id}.`); + const { message } = req; if (!message.deleted) return res.error(404, "This message is not deleted, first, delete it."); message.deleted = false; await message.save(); - res.complate(message.toObject({ virtuals: true })); + res.complate(message); }) diff --git a/routes/api/routes/threads.js b/routes/api/routes/threads.js index b5c9120..06c350c 100644 --- a/routes/api/routes/threads.js +++ b/routes/api/routes/threads.js @@ -2,22 +2,20 @@ const { MessageModel, ThreadModel } = require("../../../models"); const { Router } = require("express") const app = Router(); +app.param("id", async (req, res, next, id) => { + req.thread = await ThreadModel.get(id); -app.get("/:id", async (req, res) => { - - const { id } = req.params; - - const thread = await ThreadModel.get(id); - if (thread && (req.user?.admin || !thread.deleted)) - res.complate(thread.toObject({ virtuals: true })); - else - 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) + return res.error(404, `You do not have permissions to view this thread with id ${id}.`) + next(); }); -app.get("/:id/messages/", async (req, res) => { +app.get("/:id", async (req, res) => res.complate(req.thread)); +app.get("/:id/messages/", async (req, res) => { const { id } = req.params; const limit = Number(req.query.limit); @@ -34,68 +32,62 @@ app.get("/:id/messages/", async (req, res) => { if (!messages.length) return res.error(404, "We don't have any messages in this with your query thread."); - res.complate(messages.map(x => x.toObject({ virtuals: true }))); + res.complate(messages); }) app.post("/", async (req, res) => { - const { title = null, content = null } = req.body; + const { title, content } = req.body; if (!content || !title) return res.error(400, "Missing content/title in request body."); - const user = req.user; + const { user } = req; const thread = await new ThreadModel({ title, author: user }).takeId() const message = await new MessageModel({ content, author: user, threadID: thread.id }).takeId() await thread.push(message.id).save(); await message.save(); - res.complate(thread.toObject({ virtuals: true })); + res.complate(thread); }); app.patch("/:id/", async (req, res) => { - const thread = await ThreadModel.get(req.params.id); + const { user, thread } = req; - if (!thread || (thread.deleted && req.user && !req.user.admin)) return res.error(404, `We don't have any message with id ${req.params.id}.`); - - if (req.user.id !== thread.authorID && !req.user.admin) return res.error(403, "You have not got permission for this."); - const { title = null } = req.body; + 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."); thread.title = title; await thread.save(); - res.complate(thread.toObject({ virtuals: true })); + res.complate(thread); }) app.delete("/:id/", async (req, res) => { - const thread = await ThreadModel.get(req.params.id); - if (!thread || thread.deleted) return res.error(404, `We don't have any thread with id ${req.params.id}.`); - const user = req.user; + + const { user, thread } = req; if (user.id != thread.authorID && !user.admin) return res.error(403, "You have not got permission for this."); thread.deleted = true; await thread.save(); - res.complate(thread.toObject({ virtuals: true })); + res.complate(thread); }) app.post("/:id/undelete", async (req, res) => { - if (!req.user.admin) return res.error(403, "You have not got permission for this."); - const thread = await ThreadModel.get(req.params.id); + const { thread } = req; - if (!thread ) return res.error(404, `We don't have any thread with id ${req.params.id}.`); - if (!thread.deleted) return res.error(404, "This thread is not deleted, first, delete it."); thread.deleted = false; - thread.edited=true; + thread.edited = true; await thread.save(); - res.complate(thread.toObject({ virtuals: true })); + 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 0a0e65d..b138fd6 100644 --- a/routes/api/routes/users.js +++ b/routes/api/routes/users.js @@ -1,28 +1,36 @@ -const { UserModel } = require("../../../models"); +const { UserModel, SecretModel } = require("../../../models"); const { Router } = require("express") const app = Router(); +app.param("id", async (req, res, next, id) => { + req.member = await UserModel.get(id); + + if (!req.member) return res.error(404, `We don't have any user with id ${id}.`); + + if (req.member.deleted && !req.user?.admin) + return res.error(404, `You do not have permissions to view this user with id ${id}.`); + + next(); +}); + app.get("/:id", async (req, res) => { - const { id = null } = req.params; - - const member = await UserModel.get(id); - if (!member || (member.deleted && !req.user.admin)) return res.error(404, `We don't have any user with id ${id}.`); - + if (req.member.not()) return; res.complate(member); }); app.delete("/:id/", async (req, res) => { - const user = req.user; + const { user, member } = req; + if (req.member.not()) return; + if (!user.admin) return res.error(403, "You have not got permission for this."); const { id = null } = req.params; - const member = await UserModel.get(id); - if (!member || member.deleted) return res.error(404, `We don't have any user with id ${id}.`); + if (member.deleted) return res.error(404, `This user is with id ${id} already deleted.`); member.deleted = true; await member.save(); @@ -32,7 +40,7 @@ app.delete("/:id/", async (req, res) => { app.post("/:id/undelete/", async (req, res) => { if (!req.user.admin) return res.error(403, "You have not got permission for this."); - const member = await UserModel.get(req.params.id); + const { user, member } = req; if (!member) return res.error(404, `We don't have any user with id ${req.params.id}.`); @@ -48,16 +56,17 @@ app.post("/:id/undelete/", async (req, res) => { app.patch("/:id/", async (req, res) => { - const member = await UserModel.get(req.params.id); - - if (!member || (member.deleted && !req.user.admin)) return res.error(404, `We don't have any message with id ${req.params.id}.`); + const { user, member } = req; if (req.user.id !== member.id && !req.user.admin) return res.error(403, "You have not got permission for this."); const { avatar, name, about } = req.body; if (!avatar && !name) return res.error(400, "Missing member informations in request body."); if (avatar && /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g.test(avatar)) member.avatar = avatar; - if (name) member.name = name; + if (name) { + await SecretModel.findOneAndUpdate({ name: member.name }, { name }); + member.name = name; + } if (about) member.about = about; member.edited = true; diff --git a/routes/login.js b/routes/login.js index 12d9c03..0d15321 100644 --- a/routes/login.js +++ b/routes/login.js @@ -6,7 +6,7 @@ const bcrypt = require("bcrypt"); app.get("/", (req, res) => res.reply("login", { redirect: req.query.redirect, user: null })); app.post("/", async (req, res) => { - req.session.userid = null; + req.session.userID = null; const { username = null, password = null } = req.body; @@ -18,7 +18,7 @@ app.post("/", async (req, res) => { const member = await UserModel.findOne({ name: username }); if (!member || member.deleted) return res.error(403, 'Incorrect Username and/or Password!') - req.session.userid = user.id; + req.session.userID = user.id; res.redirect(req.query.redirect || '/'); } else diff --git a/routes/messages.js b/routes/messages.js index d91fa4d..f3d07ad 100644 --- a/routes/messages.js +++ b/routes/messages.js @@ -7,7 +7,7 @@ const app = Router(); app.get("/:id", async (req, res) => { const message = await MessageModel.get(req.params.id); - if (!message || (message.deleted && req.user && !req.user.admin)) return res.error( 404, + if (!message || (message.deleted && !req.user?.admin)) return res.error( 404, `We don't have any message with id ${req.params.id}.`); res.redirect(`/threads/${message.threadID}?scroll=${message.id}`); diff --git a/routes/register.js b/routes/register.js index be99f9d..d7750bc 100644 --- a/routes/register.js +++ b/routes/register.js @@ -11,7 +11,7 @@ app.post("/", rateLimit({ windowMs: 24 * 60 * 60_000, max: 10, standardHeaders: true, legacyHeaders: false, handler: (_r, response, _n, options) => response.error(options.statusCode, "You are begin ratelimited") }), async (req, res) => { - req.session.userid = null; + req.session.userID = null; let { username = null, password: body_pass = null, avatar, about } = req.body; @@ -33,7 +33,7 @@ app.post("/", rateLimit({ const salt = await bcrypt.genSalt(10); const password = await bcrypt.hash(body_pass, salt); await SecretModel.create({ username, password, id: user2.id }) - req.session.userid = user2.id; + req.session.userID = user2.id; res.redirect('/'); diff --git a/routes/threads.js b/routes/threads.js index f89fecf..b10814b 100644 --- a/routes/threads.js +++ b/routes/threads.js @@ -6,8 +6,8 @@ const { ThreadModel, MessageModel } = require("../models") app.get("/", async (req, res) => { - const threads = await ThreadModel.find(req.user?.admin ? {} : { deleted: false })//.limit(10); - + let threads = await ThreadModel.find(req.user?.admin ? {} : { deleted: false })//.limit(10); + threads = await Promise.all(threads.map(thread => thread.get_author())); return res.reply("threads", { threads }); }); @@ -18,7 +18,7 @@ app.get("/:id/", async (req, res) => { const { user, params: { id } } = req - let page = Number(req.query.page || 0); + const page = Number(req.query.page || 0); const thread = await ThreadModel.get(id) thread.count = await thread.messageCount(user?.admin); @@ -28,15 +28,14 @@ app.get("/:id/", async (req, res) => { const query = { threadID: id }; if (!user || !user.admin) query.deleted = false; - const messages = await MessageModel.find(query).sort({ time: 1 }).limit(10).skip(page * 10) - .then(messages => messages.map(message => { + const messages = await Promise.all(await MessageModel.find(query).sort({ time: 1 }).limit(10).skip(page * 10) + .then(messages => messages.map(async message => { message.content = message.content.replaceAll("&", "&") .replaceAll("<", "<").replaceAll(">", ">") .replaceAll("\"", """).replaceAll("'", "'") .replaceAll("\n", "
"); - return message.toObject({ virtuals: true }); - })) - + return await message.get_author(); + }))); res.reply("thread", { page, thread, messages, scroll: req.query.scroll || thread.messages[0].id }); thread.save();