From c110b195a3ed87bc8c459879f75870b78826c00c Mon Sep 17 00:00:00 2001 From: Akif9748 Date: Thu, 1 Sep 2022 14:02:47 +0300 Subject: [PATCH] Better edits, already deleted, global ratelimit --- APIDOCS.md | 1 - README.md | 20 ++---- index.js | 11 ++- models/Secret.js | 2 +- public/css/thread.css | 7 +- public/js/thread.js | 52 ++++++++++----- routes/api/routes/messages.js | 15 +++-- routes/api/routes/threads.js | 1 + routes/api/routes/users.js | 44 ++++-------- routes/login.js | 5 +- routes/register.js | 5 +- routes/users.js | 11 +++ views/admin.ejs | 96 +++++++++++++------------- views/create_thread.ejs | 40 +++++------ views/edit_user.ejs | 51 ++++++++++++++ views/error.ejs | 2 +- views/extra/meta.ejs | 18 +++-- views/extra/navbar.ejs | 84 +++++++++++------------ views/index.ejs | 90 ++++++++++++------------- views/login.ejs | 17 +++-- views/register.ejs | 24 +++---- views/thread.ejs | 122 +++++++++++++++++----------------- views/threads.ejs | 48 ++++++------- views/user.ejs | 105 ++++++++++------------------- views/users.ejs | 15 ++--- 25 files changed, 455 insertions(+), 431 deletions(-) create mode 100644 views/edit_user.ejs diff --git a/APIDOCS.md b/APIDOCS.md index 1df62d1..04231eb 100644 --- a/APIDOCS.md +++ b/APIDOCS.md @@ -25,7 +25,6 @@ But in front end, the API will works with session. - 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. - GET `/api/threads/:id` for fetch thread. - DELETE `/api/threads/:id/` for delete thread. diff --git a/README.md b/README.md index 8255e81..c37c0f7 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Run `node util/reset` to **reset the database**, and run `node util/admin` for g Edit `config.json` for default themes of users... ## API -Akf-forum has got an API for AJAX, other clients etc. And, you can learn about API in `util/APIDOCS.md`. +Akf-forum has got an API for AJAX (fetch), other clients etc. And, you can learn about API in `util/APIDOCS.md`. ## Credits * [Akif9748](https://github.com/Akif9748) - Project mainteiner, main developer, made **old** frontend @@ -32,23 +32,17 @@ Akf-forum has got an API for AJAX, other clients etc. And, you can learn about A | 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 | +| Footer | 🟡 | LOW | -- Profile photos will store in database +- Better Auth +- Profile photos will store in a folder - replacer function global -- author name of thread - page for threads - users -- extra ratelimits -- better edits -- IP BAN fix, user -> ips [] +- IPs of users will add SecretModel - message counts for API -- ZATEN SİLİNDİ BU KİŞİ & MESAJ -- delete admin request, moreover, add it to user patch delete 😳, better theme patch +- better theme patch UserModel +- ajax, delete update thread dom ### API | To do | Is done? diff --git a/index.js b/index.js index e3dc9b2..db57fcd 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,7 @@ const { UserModel, BanModel } = require("./models"), express = require('express'), fs = require("fs"), app = express(); +const rateLimit = require('express-rate-limit') app.ips = []; @@ -17,10 +18,11 @@ mongoose.connect(process.env.MONGO_DB_URL, app.set("view engine", "ejs"); -app.use(session({ secret: 'secret', resave: true, saveUninitialized: true }), - bodyParser.urlencoded({ extended: true }), +app.use( + session({ secret: 'secret', resave: true, saveUninitialized: true }), express.static("public"), express.json(), ipBlock(app.ips), async (req, res, next) => { + req.headers["x-forwarded-for"] 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 }); @@ -32,7 +34,10 @@ app.use(session({ secret: 'secret', resave: true, saveUninitialized: true }), return res.error(403, "Your account has been deleted."); } next(); - } + }, rateLimit({ + windowMs: 60_000, max: 10, + handler: (req, res, _n, opts) => !req.user.admin ? res.error(opts.statusCode, "You are begin ratelimited") : next() + }), bodyParser.urlencoded({ extended: true }) ); for (const file of fs.readdirSync("./routes")) diff --git a/models/Secret.js b/models/Secret.js index e8c71e0..c42e701 100644 --- a/models/Secret.js +++ b/models/Secret.js @@ -2,7 +2,7 @@ const mongoose = require("mongoose") const schema = new mongoose.Schema({ username: { type: String, unique: true }, - password: String, + password: String, ips: [String], id: { type: String, unique: true } }); diff --git a/public/css/thread.css b/public/css/thread.css index daa33d8..48b2aca 100644 --- a/public/css/thread.css +++ b/public/css/thread.css @@ -149,7 +149,12 @@ } - +.send>textarea{ + font-family:inherit; + width: 100%; + margin: 10px; + border: 2px solid #e3e3e3; +} /* Media Query */ @media(max-width:980px) { diff --git a/public/js/thread.js b/public/js/thread.js index 0dad06b..1ea48c0 100644 --- a/public/js/thread.js +++ b/public/js/thread.js @@ -1,5 +1,7 @@ import request from "./request.js"; +// THREAD: + window.edit_thread = async function (id) { const title = prompt("Enter new title!"); const res = await request(`/api/threads/${id}/`, "PATCH", { title }); @@ -11,7 +13,6 @@ window.edit_thread = async function (id) { window.delete_thread = async function (id) { const res = await request(`/api/threads/${id}/`, "DELETE"); if (res.error) return; - alert(`Thread deleted`); location.reload(); } @@ -19,41 +20,56 @@ window.delete_thread = async function (id) { window.undelete_thread = async function (id) { const res = await request(`/api/threads/${id}/undelete`); if (res.error) return; - alert(`Thread undeleted`); location.reload(); } -window.edit_message = async function (id) { - const content = prompt("Enter new content!"); - const res = await request(`/api/messages/${id}/`, "PATCH", { content }); - if (res && res.error) return; + +// MESSAGES: +window.send_edit = async function (id) { + const message = document.getElementById(`message-${id}`); + const content = message.querySelector("#content").value; + + const res = await request(`/api/messages/${id}/`, "PATCH", { content }); + if (res.error) return; alert(`Message updated`); - document.getElementById("message-" + id).querySelector(".content").innerHTML = content; + message.querySelector(".content").innerHTML = res.content; +} +window.edit_message = async function (id) { + const content = document.getElementById(`message-${id}`).querySelector(".content"); + + content.innerHTML = ` + + `; } window.undelete_message = async function (id) { const response = await request(`/api/messages/${id}/undelete`); if (response.deleted) return; - document.getElementById("deleted-" + id).remove(); - document.getElementById("dot-" + id).innerHTML = ` + const message = document.getElementById("message-" + id); + + message.querySelector("#deleted").remove(); + message.querySelector(".dots-menu").innerHTML = ` DELETE EDIT` } window.delete_message = async function (id) { const response = await request(`/api/messages/${id}/`, "DELETE"); - if (response.deleted) { - alert("Message deleted"); - document.getElementById("dots-" + id).innerHTML = ` - - `+ document.getElementById("dots-" + id).innerHTML; - document.getElementById("dot-" + id).innerHTML = `UNDELETE`; - } + if (!response.deleted) return + const message = document.getElementById(`message-${id}`); + alert("Message deleted"); + + message.querySelector(".dots-menu").innerHTML = `UNDELETE`; + + let dots = message.querySelector(".dots"); + dots.innerHTML = "" + dots.innerHTML; + } window.react = async function (id, type) { const res = await request(`/api/messages/${id}/react/${type}`) - document.getElementById(`like-${id}`).innerHTML = res.react.like.length; - document.getElementById(`dislike-${id}`).innerHTML = res.react.dislike.length; + const message = document.getElementById(`message-${id}`); + for (const react in res.react) + message.querySelector("#" + react).innerHTML = res.react[react].length; } diff --git a/routes/api/routes/messages.js b/routes/api/routes/messages.js index bca3877..88dda90 100644 --- a/routes/api/routes/messages.js +++ b/routes/api/routes/messages.js @@ -10,20 +10,20 @@ app.param("id", async (req, res, next, id) => { if (!req.message) return res.error(404, `We don't have any message with id ${id}.`); - if(req.message.deleted && !req.user?.admin) + 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) => { - + res.complate(message); }) app.patch("/:id/", async (req, res) => { - + const { message, user } = req; @@ -59,7 +59,7 @@ app.post("/", rateLimit({ }) app.post("/:id/react/:type", async (req, res) => { - + const { message } = req; @@ -91,12 +91,13 @@ app.post("/:id/react/:type", async (req, res) => { }); app.delete("/:id/", async (req, res) => { - + const { message, user } = req; if (user.id != message.authorID && !user.admin) return res.error(403, "You have not got permission for this."); + if (message.deleted) return res.error(403, "This message is already deleted."); message.deleted = true; await message.save(); @@ -105,7 +106,7 @@ app.delete("/:id/", async (req, res) => { }) app.post("/:id/undelete", async (req, res) => { - + const { message } = req; diff --git a/routes/api/routes/threads.js b/routes/api/routes/threads.js index 827c9f4..397a7ec 100644 --- a/routes/api/routes/threads.js +++ b/routes/api/routes/threads.js @@ -70,6 +70,7 @@ 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; await thread.save(); console.log(thread) diff --git a/routes/api/routes/users.js b/routes/api/routes/users.js index 293ce6b..3434ff6 100644 --- a/routes/api/routes/users.js +++ b/routes/api/routes/users.js @@ -14,16 +14,10 @@ app.param("id", async (req, res, next, id) => { next(); }); -app.get("/:id", async (req, res) => { - - if (req.member.not()) return; - res.complate(member); - -}); +app.get("/:id", async (req, res) => res.complate(req.member)); app.delete("/:id/", async (req, res) => { const { user, member } = req; - if (req.member.not()) return; if (!user.admin) return res.error(403, "You have not got permission for this."); @@ -55,22 +49,28 @@ app.post("/:id/undelete/", async (req, res) => { app.patch("/:id/", async (req, res) => { - 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, theme } = req.body; - if (!avatar && !name && !about && !theme) return res.error(400, "Missing member informations in request body."); + if (req.user.id !== member.id && !user.admin) return res.error(403, "You have not got permission for this."); + if (!Object.values(req.body).some(Boolean)) return res.error(400, "Missing member informations in request body."); + + const { avatar, name, about, theme, admin } = req.body; + + if (admin?.length && !req.user.admin) return res.error(403, "You have not got permission for edit 'admin' information, or bad request."); + 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) { await SecretModel.findOneAndUpdate({ name: member.name }, { name }); member.name = name; } if (about) member.about = about; - if (theme) member.theme = member.theme === "default" ? "black" : "default"; - member.theme = theme; + if (theme) + member.theme = member.theme === "default" ? "black" : "default"; + + if(typeof admin === "boolean" || ["false","true"].includes(admin)) member.admin = admin; member.edited = true; await member.save(); @@ -79,22 +79,4 @@ app.patch("/:id/", async (req, res) => { }) -app.post("/:id/admin/", async (req, res) => { - - const user = req.user; - - if (!user.admin) return res.error(403, "You have not got permission for this."); - const user2 = await UserModel.get(req.params.id); - - if (!user2) - return res.error(404, `We don't have any user with id ${id}.`); - - - user2.admin = true; - await user2.save() - - - res.complate(user2); - -}); module.exports = app; \ No newline at end of file diff --git a/routes/login.js b/routes/login.js index 0d15321..ccf3daf 100644 --- a/routes/login.js +++ b/routes/login.js @@ -28,9 +28,6 @@ app.post("/", async (req, res) => { } else res.error(400, "You forgot entering some values") - - -}) - +}); module.exports = app; \ No newline at end of file diff --git a/routes/register.js b/routes/register.js index d7750bc..d943c5c 100644 --- a/routes/register.js +++ b/routes/register.js @@ -8,11 +8,10 @@ const app = Router(); app.get("/", (req, res) => res.reply("register", { user: null })); app.post("/", rateLimit({ - windowMs: 24 * 60 * 60_000, max: 10, standardHeaders: true, legacyHeaders: false, + windowMs: 24 * 60 * 60_000, max: 5, 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.destroy() let { username = null, password: body_pass = null, avatar, about } = req.body; diff --git a/routes/users.js b/routes/users.js index 675d8e6..d4def5c 100644 --- a/routes/users.js +++ b/routes/users.js @@ -9,6 +9,17 @@ app.get("/", async ({ user }, res) => { }); +app.get("/:id/edit", async (req, res) => { + if(!req.user || (!req.user.admin&&req.params.id !== req.user.id)) return res.error(403, "You have not got permission for this."); + const member = await UserModel.get(req.params.id); + + if (member && (req.user?.admin || !member.deleted)) + res.reply("edit_user", { member }) + else + res.error(404, `We don't have any user with id ${req.params.id}.`); + +}); + app.get("/:id", async (req, res) => { const user = req.user const { id } = req.params; diff --git a/views/admin.ejs b/views/admin.ejs index 704f0e8..d379280 100644 --- a/views/admin.ejs +++ b/views/admin.ejs @@ -12,66 +12,68 @@ border-collapse: collapse; width: 100%; } - - td, th { + + td, + th { border: 1px solid #dddddd; text-align: left; padding: 8px; color: var(--reaction-hover); } - + tr:nth-child(even) { background-color: #dddddd; } - +

Welcome to the admin panel of the forum, <%= user.name %>!

- + IP BAN REMOVE IP BAN - +

Banned users:

- - - - - - - <% for (const ban of bans) { %> - - - - - - <% } %> -
IPReasonAuthorID
<%=ban.ip%><%=ban.reason%><%=ban.authorID%>
- - - + + + + + + + <% for (const ban of bans) { %> + + + + + + <% } %> +
IPReasonAuthorID
<%=ban.ip%><%=ban.reason%><%=ban.authorID%>
+ + +
- - - + + + + \ No newline at end of file diff --git a/views/create_thread.ejs b/views/create_thread.ejs index a757a4c..3b23a43 100644 --- a/views/create_thread.ejs +++ b/views/create_thread.ejs @@ -3,8 +3,6 @@ <%- include("extra/meta", {title: "Create thread!" }) %> - - @@ -15,37 +13,35 @@

Title:

- +

Content:

- - - + + + - + - + \ No newline at end of file diff --git a/views/edit_user.ejs b/views/edit_user.ejs new file mode 100644 index 0000000..95e82ef --- /dev/null +++ b/views/edit_user.ejs @@ -0,0 +1,51 @@ + + + +<%- include("extra/meta", {title: "Edit "+member.name+"!" }) %> + + + + + + + <%- include("extra/navbar") %> + +

Edit <%= member.name %>

+ +
+
+ + + + <% if (user.admin){ %> + Is Admin? > + + <% } %> + + +
+ +
+ + + + + + + \ No newline at end of file diff --git a/views/error.ejs b/views/error.ejs index 5ad849b..ae6952e 100644 --- a/views/error.ejs +++ b/views/error.ejs @@ -1,7 +1,7 @@ -<%- include("extra/meta", {title: "User list!" }) %> +<%- include("extra/meta", {title: type+" error!" }) %> diff --git a/views/extra/meta.ejs b/views/extra/meta.ejs index 391d0e4..66aaa87 100644 --- a/views/extra/meta.ejs +++ b/views/extra/meta.ejs @@ -1,9 +1,13 @@ - - - <%= title || "Akf-forum" %> - - - - + + + <%= title || "Akf-forum" %> + + + + + <% if (theme === "black") { %> + + <% } %> + \ No newline at end of file diff --git a/views/extra/navbar.ejs b/views/extra/navbar.ejs index 0543453..475df87 100644 --- a/views/extra/navbar.ejs +++ b/views/extra/navbar.ejs @@ -2,56 +2,58 @@ <% if (user?.admin){ %>
- You are admin, and you can go your page! + You are admin, and you can go your page!
<% } %>
- -
- + +
+ <% if (user){ %> - -
<%= user.name %> -
-
-
- Logout - <%=(user.theme === "default" ? "black" : "default" ) + " mode" %> - + +
<%= user.name %> +
+
+
+ Logout + <%=(user.theme === "default" ? "black" : "default" ) + " mode" %> + <% } else { %> - - Login - Register - - <% } %> - -
+ + Login + Register + + <% } %> + +
\ No newline at end of file diff --git a/views/index.ejs b/views/index.ejs index 7d4b851..a05e114 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -4,58 +4,58 @@ <%- include("extra/meta", {title: "Welcome to the Akf-forum!" }) %> - - + + <%- include("extra/navbar") %> - -
- <% if (user) { %> -

Welcome, <%= user.name %> -
-
- You can log out of the site here: +
+ <% if (user) { %> +

+
Welcome, <%= user.name %> +
-

- - LOGOUT -
- <% } else { %> -

Welcome, Guest!
You can press the button to register: - REGISTER - -

- <% } %> - -

Statistics:

+
+ You can log out of the site here: -
-

Message count:

-

<%= messages %>

-
- -
-

User count:

-

<%= users %>

-
- -
-

Thread count:

-

<%= threads %>

-
- -
-

Memory usage:

-

<%= mem.toFixed(2); %> MB

-
- -

- <%- include("extra/footer") %> + + + LOGOUT +
+ <% } else { %> +

Welcome, Guest!
You can press the button to register: + REGISTER + +

+ <% } %> + +

Statistics:

+ +
+

Message count:

+

<%= messages %>

+
+ +
+

User count:

+

<%= users %>

+
+ +
+

Thread count:

+

<%= threads %>

+
+ +
+

Memory usage:

+

<%= mem.toFixed(2); %> MB

+
+ + + <%- include("extra/footer") %> - - + \ No newline at end of file diff --git a/views/login.ejs b/views/login.ejs index 79afa11..a5b152a 100644 --- a/views/login.ejs +++ b/views/login.ejs @@ -1,4 +1,3 @@ - @@ -8,18 +7,18 @@ - <%- include("extra/navbar") %> + <%- include("extra/navbar") %> -

Login

+

Login

" method="post"> - - - - + + + +
- + - + \ No newline at end of file diff --git a/views/register.ejs b/views/register.ejs index 6a9298b..1ce0f88 100644 --- a/views/register.ejs +++ b/views/register.ejs @@ -5,26 +5,24 @@ - + - <%- include("extra/navbar") %> + <%- include("extra/navbar") %> -

Register

+

Register

-
+ - - - - + - + + + + +
- - - - + \ No newline at end of file diff --git a/views/thread.ejs b/views/thread.ejs index 73fcfbf..aea6cf2 100644 --- a/views/thread.ejs +++ b/views/thread.ejs @@ -16,73 +16,74 @@
<%= thread.title %>
- <%= new Date(thread.time).toLocaleString() %> • Views: <%= thread.views %> <%= "• "+(thread.edited ? "Edited" : "Not edited")%> + <%= new Date(thread.time).toLocaleString() %> • Views: <%= thread.views %>%> +
+
+ <%= thread.author.name %> <%= "• "+(thread.edited ? "Edited" : "Not edited")%>
-
- - - <% if (user && !thread.deleted){ %> + - DELETE - EDIT + <% if (user && !thread.deleted){ %> + + DELETE + EDIT <% } else if (thread.deleted) { %>

This thread has been deleted

- UNDELETE - + UNDELETE + <% }; %>
-
+
<% messages.filter(Boolean).forEach(message=>{ %>
- +
- <%= new Date(message.time).toLocaleDateString() %> + <%= new Date(message.time).toLocaleDateString() %>
- <%= new Date(message.time).toLocaleTimeString() %> + <%= new Date(message.time).toLocaleTimeString() %>
-
+
<%- message.content %>
<% if(user){ %> <% if(user.id === message.authorID || user.admin){ %> -
+
<% if (message.deleted){ %> - + <% } %> <% if (message.edited){ %> - + <% } %> - +
-
- <% if (!message.deleted){ %> +
+ <% if (!message.deleted){ %> Delete Edit - <% }else { %> + <% }else { %> Undelete - <% } %> + <% } %>
<% } %> -
- -
<%=message.react.like.length %>
+ +
<%=message.react.like.length %>
- -
<%=message.react.dislike.length %>
+ +
<%=message.react.dislike.length %>
<% }; %> @@ -93,48 +94,46 @@
<% if (user){ %> -
+
- + - - - +
+
+ <% }%> - + \ No newline at end of file diff --git a/views/threads.ejs b/views/threads.ejs index 117e10b..eaa405d 100644 --- a/views/threads.ejs +++ b/views/threads.ejs @@ -3,34 +3,34 @@ <%- include("extra/meta", {title: "Thread list!" }) %> - - + + -<%- include("extra/navbar") %> + <%- include("extra/navbar") %> -
+
- <% threads.forEach(thread=>{ %> + <% threads.forEach(thread=>{ %> -
-
- <% if (thread.deleted) { %> [DELETED]<% } %> - <%= thread.title %> -
-
- <% if (user && !thread.deleted){ %> - - <% } %> - <%= thread.author.name %>
- -
- +
+
+ <% if (thread.deleted) { %> [DELETED]<% } %> + <%= thread.title %>
- -
- <% }); %> - -
+
+ <% if (user && !thread.deleted){ %> + + <% } %> + <%= thread.author.name %>
- +
+ +
+ +
+ <% }); %> + +
+ + \ No newline at end of file diff --git a/views/user.ejs b/views/user.ejs index a37a909..77dc295 100644 --- a/views/user.ejs +++ b/views/user.ejs @@ -6,18 +6,47 @@ - + <%if (user) {%> + + <% }; %> <%- include("extra/navbar") %>
+ + <% if (user?.admin || user?.id === member.id) { %> + Edit user! + <% } %> + + <% if (member.deleted) {%> +

This user has been deleted!

+ Undelete user! + <% } else if (user?.admin){ %> + Delete user! + <% }; %> + +
- +

Name:

<%= member.name %>

-

Created at:

<%= new Date(member.time).toLocaleString() %>

@@ -26,9 +55,8 @@

Is admin?

<%= member.admin ? "Yes" : "No" %>

-
-

Message:

+

Message:

<%= counts.message %>

@@ -42,72 +70,7 @@ <%= member.about %>

- <% if (user?.admin && !member.deleted) {%> - Change name of the user! - Change avatar of the user! - Change about of the user! - Give admin permissions! - Delete user! - - - <% }; %> - <% if (member.deleted) {%> -

This user has been deleted!

- Undelete user! - - - <% }; %> -
- + \ No newline at end of file diff --git a/views/users.ejs b/views/users.ejs index e30ae90..7123e51 100644 --- a/views/users.ejs +++ b/views/users.ejs @@ -10,17 +10,16 @@ <%- include("extra/navbar") %>
- <% users.forEach(user=>{ %> -
+ <% users.forEach(user=>{ %> +
+ <% if (user.deleted) { %> [DELETED]<% } %> + <%= user.name %>
-
- <% }); %> +
+ <% }); %>
- - + \ No newline at end of file