Caching for users, and rename is fixed

This commit is contained in:
Akif9748 2022-08-31 14:44:28 +03:00
parent 256b70c611
commit b0a7ac7605
15 changed files with 157 additions and 169 deletions

View file

@ -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```

View file

@ -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.

View file

@ -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 });

View file

@ -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 });
model.get = async id => {
const message = await model.findOne({ id })
return await message.get_author();
};
module.exports = model;

View file

@ -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;

View file

@ -14,7 +14,6 @@ const schema = new mongoose.Schema({
});
schema.methods.takeId = async function () {
this.id = String(await model.count() || 0);
return this;

14
models/cache.js Normal file
View file

@ -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;
}

View file

@ -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 })

View file

@ -5,23 +5,29 @@ 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);
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 { message, user } = req;
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;
@ -29,19 +35,17 @@ app.patch("/:id/", async (req, res) => {
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);
})

View file

@ -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,59 +32,53 @@ 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);
if (!thread ) return res.error(404, `We don't have any thread with id ${req.params.id}.`);
const { thread } = req;
if (!thread.deleted) return res.error(404, "This thread is not deleted, first, delete it.");
@ -95,7 +87,7 @@ app.post("/:id/undelete", async (req, res) => {
await thread.save();
res.complate(thread.toObject({ virtuals: true }));
res.complate(thread);
})
module.exports = app;

View file

@ -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;

View file

@ -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

View file

@ -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}`);

View file

@ -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('/');

View file

@ -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("<", "&lt;").replaceAll(">", "&gt;")
.replaceAll("\"", "&quot;").replaceAll("'", "&#39;")
.replaceAll("\n", "<br>");
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();