Better edits, already deleted, global ratelimit

This commit is contained in:
Akif9748 2022-09-01 14:02:47 +03:00
parent cd801c94c7
commit c110b195a3
25 changed files with 455 additions and 431 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -149,7 +149,12 @@
}
.send>textarea{
font-family:inherit;
width: 100%;
margin: 10px;
border: 2px solid #e3e3e3;
}
/* Media Query */
@media(max-width:980px) {

View file

@ -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 = `
<textarea rows="4" cols="40" id="content">${content.innerHTML}</textarea>
<button onclick="send_edit(${id});" class="btn-primary">Edit!</button>`;
}
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 = `
<a onclick="delete_message('${id}');">DELETE</a>
<a onclick="edit_message('${id}');">EDIT</a>`
}
window.delete_message = async function (id) {
const response = await request(`/api/messages/${id}/`, "DELETE");
if (response.deleted) {
if (!response.deleted) return
const message = document.getElementById(`message-${id}`);
alert("Message deleted");
document.getElementById("dots-" + id).innerHTML = `
<i class='bx bx-trash bx-sm' id="deleted-${id}" style="color: var(--important)"></i>
`+ document.getElementById("dots-" + id).innerHTML;
document.getElementById("dot-" + id).innerHTML = `<a onclick="undelete_message('${id}');">UNDELETE</a>`;
}
message.querySelector(".dots-menu").innerHTML = `<a onclick="undelete_message('${id}');">UNDELETE</a>`;
let dots = message.querySelector(".dots");
dots.innerHTML = "<i class='bx bx-trash bx-sm' id='deleted' style='color: var(--important)'></i>" + 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;
}

View file

@ -97,6 +97,7 @@ app.delete("/:id/", async (req, res) => {
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();

View file

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

View file

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

View file

@ -28,9 +28,6 @@ app.post("/", async (req, res) => {
} else
res.error(400, "You forgot entering some values")
})
});
module.exports = app;

View file

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

View file

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

View file

@ -13,7 +13,8 @@
width: 100%;
}
td, th {
td,
th {
border: 1px solid #dddddd;
text-align: left;
padding: 8px;
@ -74,4 +75,5 @@
</div>
</body>
</html>

View file

@ -3,8 +3,6 @@
<%- include("extra/meta", {title: "Create thread!" }) %>
<body style="text-align: center;">
<link rel="stylesheet" href="/css/create_thread.css" />
@ -25,7 +23,6 @@
<script type="module">
import request from "../../js/request.js";
document.addEventListener("submit", async e => {
@ -34,7 +31,8 @@
const data = new FormData(form);
const response = await request("/api/threads/", "POST", {
title: data.get("title"), content: data.get("content")
title: data.get("title"),
content: data.get("content")
});
@ -43,8 +41,6 @@
});
</script>
</body>

51
views/edit_user.ejs Normal file
View file

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<%- include("extra/meta", {title: "Edit "+member.name+"!" }) %>
<body style="text-align: center;">
<link rel="stylesheet" href="/css/login.css" />
<link rel="stylesheet" href="/css/user.css" />
<%- include("extra/navbar") %>
<h1 class="title">Edit <a href="/users/<%= member.id %>"><%= member.name %></a></h1>
<div class="content">
<form id="form">
<input type="text" name="name" placeholder="<%=member.name%>" class="input">
<input type="url" name="avatar" placeholder="<%=member.avatar%>" class="input">
<textarea class="input" name="about" rows="4" name="content" placeholder="<%=member.about%>"></textarea>
<% if (user.admin){ %>
Is Admin? <input id='admin' type='checkbox' value='true' name='admin' <%=member.admin ? "checked": ""%>>
<input id='adminHidden' type='hidden' value='false' name='admin'>
<% } %>
<button class="btn-primary" style="width:100%;">Update User!</button>
</form>
</div>
<script type="module">
const form = document.getElementById("form");
import request from "../../js/request.js";
form.addEventListener("submit", async e => {
e.preventDefault();
document.getElementById('adminHidden').disabled = document.getElementById("admin").checked;
const object = {};
new FormData(e.target).forEach((value, key) => object[key] = value);
console.log(object)
const res = await request("/api/users/<%=member.id%>", "PATCH", object);
if (res) alert(`User is updated!`);
location.reload();
});
</script>
</body>
</html>

View file

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<%- include("extra/meta", {title: "User list!" }) %>
<%- include("extra/meta", {title: type+" error!" }) %>
<body style="text-align: center;">

View file

@ -6,4 +6,8 @@
<meta name="author" content="Akif9748">
<link rel="icon" type="image/x-icon" href="/images/favicon.jpg">
<link rel="stylesheet" href="/css/themes/<%= theme %>.css" />
<% if (theme === "black") { %>
<meta name="theme-color" content="#000000" />
<% } %>
</head>

View file

@ -21,8 +21,11 @@
<a onclick="invert()" class="btn-outline-primary"><%=(user.theme === "default" ? "black" : "default" ) + " mode" %></a>
<script>
async function invert() {
await fetch('/api/users/<%= user.id %>',{method:'PATCH',
body:JSON.stringify({theme:"<%=user.theme === `default` ? `black` : `default` %>"}),
await fetch('/api/users/<%= user.id %>', {
method: 'PATCH',
body: JSON.stringify({
theme: "<%=user.theme === `default` ? `black` : `default` %>"
}),
headers: {
"Content-Type": "application/json"
}
@ -52,6 +55,5 @@
for (let i = 0; i < menuItems.length; i++)
if (menuItems[i].getAttribute("href") == window.location.pathname)
menuItems[i].classList.add("active-menu");
</script>
</div>

View file

@ -13,7 +13,8 @@
<div class="content">
<% if (user) { %>
<h2 style="color: var(--main);"><div class="box-username">Welcome, <%= user.name %>
<h2 style="color: var(--main);">
<div class="box-username">Welcome, <%= user.name %>
<div class="avatar"><img src="<%=user.avatar %>"></div>
</div>
@ -58,4 +59,3 @@
</body>
</html>

View file

@ -1,4 +1,3 @@
</html>
<!DOCTYPE html>
<html lang="en">

View file

@ -17,10 +17,8 @@
<input type="text" name="username" placeholder="Username" class="input" required>
<input type="password" name="password" placeholder="Password" class="input" required>
<input type="text" name="about" placeholder="About you... Not required" class="input">
<input type="text" name="avatar" placeholder="Avatar URL (not required)" class="input">
<input type="url" name="avatar" placeholder="Avatar URL (not required)" class="input">
<textarea class="input" name="about" rows="4" placeholder="About you... Not required"></textarea>
<input type="submit" class="btn-primary" style="width:100%;" value="Register">
</form>

View file

@ -16,9 +16,11 @@
<div style="text-align:center;padding:8px">
<div class="title" id="title"><%= thread.title %></div>
<div class="date">
<%= new Date(thread.time).toLocaleString() %> • Views: <%= thread.views %> <%= "• "+(thread.edited ? "Edited" : "Not edited")%>
<%= new Date(thread.time).toLocaleString() %> • Views: <%= thread.views %>%>
</div>
<div class="date">
<a style="color: var(--reaction-hover);" href="/users/<%= thread.author.id %>"><%= thread.author.name %></a> <%= "• "+(thread.edited ? "Edited" : "Not edited")%>
</div>
</div>
<div style="text-align:center;padding:8px">
<!-- THREAD AUTHOR AND PROFILE PHOTO -->
@ -34,7 +36,7 @@
<% }; %>
</div>
<div id="messages" value="<%= thread.id %>">
<div id="messages">
<% messages.filter(Boolean).forEach(message=>{ %>
@ -54,17 +56,17 @@
<% if(user){ %>
<% if(user.id === message.authorID || user.admin){ %>
<div class="dots" id="dots-<%=message.id %>" onclick="dots('<%=message.id %>')">
<div class="dots" onclick="dots('<%=message.id %>')">
<% if (message.deleted){ %>
<i class='bx bx-trash bx-sm' id="deleted-<%=message.id %>" style="color: var(--important);"></i>
<i class='bx bx-trash bx-sm' id="deleted" style="color: var(--important);"></i>
<% } %>
<% if (message.edited){ %>
<i class='bx bx-pencil bx-sm' id="edited-<%=message.id %>" style="color: GREEN;"></i>
<i class='bx bx-pencil bx-sm' id="edited" style="color: GREEN;"></i>
<% } %>
<i class='bx bx-dots-horizontal-rounded'></i>
</div>
<div class="dots-menu" id="dot-<%=message.id %>">
<div class="dots-menu">
<% if (!message.deleted){ %>
<a onclick="delete_message('<%=message.id %>');">Delete</a>
<a onclick="edit_message('<%=message.id %>');">Edit</a>
@ -74,15 +76,14 @@
</div>
<% } %>
<div class="reactions">
<div>
<i onclick='react("<%= message.id %>","like");' class='bx bx-like'></i>
<div id="like-<%= message.id %>"><%=message.react.like.length %></div>
<div id="like"><%=message.react.like.length %></div>
</div>
<div>
<i onclick='react("<%= message.id %>","dislike");' class='bx bx-dislike'></i>
<div id="dislike-<%= message.id %>"><%=message.react.dislike.length %></div>
<div id="dislike"><%=message.react.dislike.length %></div>
</div>
</div>
<% }; %>
@ -93,17 +94,16 @@
</div>
<% if (user){ %>
<div class="message">
<div class="message" id="send-div">
<form id="send" style="width:100%">
<textarea rows="4"
style="
font-family:inherit;
width: 100%;
margin: 10px;
border: 2px solid #e3e3e3;" name="content"></textarea>
<textarea rows="4" name="content"></textarea>
<input name="threadID" type="hidden" value="<%= thread.id %>"></input>
<input name="page" type="hidden" value="<%= page %>"></input>
<button class="btn-primary">Send!</button>
</form>
</div>
<script type="module">
import request from "../../js/request.js";
@ -113,20 +113,19 @@
const data = new FormData(e.target);
request("/api/messages", "POST", { threadID: "<%= thread.id %>", content: data.get("content") })
.then(res => {
const res = await request("/api/messages", "POST", {
threadID: "<%= thread.id %>",
content: data.get("content")
})
let tp = Number("<%= thread.pages %>")
let tm = Number("<%= thread.count %>")
if (tp * 10 === tm) tp++;
if (res) location.href = `/threads/${data.get("threadID")}?page=${tp-1}`;
});
});
</script>
<button class="btn-primary">Send!</button>
</form>
</div>
<% }%>
<div class="pagination">
<div class="back">
@ -151,8 +150,9 @@
<script>
document.getElementById("message-<%= scroll %>").scrollIntoView();
function dots(id) {
document.getElementById('dot-'+id).classList.toggle('active')
document.getElementById('message-' + id).querySelector(".dots-menu").classList.toggle('active')
}
</script>

View file

@ -6,10 +6,40 @@
<body>
<link rel="stylesheet" href="/css/user.css" />
<%if (user) {%>
<script type="module">
import request from "../../js/request.js";
document.addEventListener("click", async e => {
if (e.target.id == "delete") {
const response = await request("/api/users/<%= member.id %>", "DELETE");
if (!response.deleted) return
alert("User is deleted!");
location.reload()
} else if (e.target.id == "undelete") {
const response = await request("/api/users/<%= member.id %>/undelete");
if (response.deleted) return;
alert("User is undeleted successfully!");
location.reload()
}
});
</script>
<% }; %>
<%- include("extra/navbar") %>
<div class="content">
<% if (user?.admin || user?.id === member.id) { %>
<a class="btn-outline-primary" href="<%= member.getLink() %>/edit">Edit user!</a>
<% } %>
<% if (member.deleted) {%>
<h1>This user has been deleted!</h1>
<a id="undelete" class="btn-primary">Undelete user! </a>
<% } else if (user?.admin){ %>
<a id="delete" class="btn-outline-primary">Delete user! </a>
<% }; %>
<div class="box" style="justify-content:center;">
<img style="width:100px;height:100px;border-radius:50%;" src="<%=member.avatar %>">
</div>
@ -17,7 +47,6 @@
<h2 class="box-title">Name:</h2>
<h2 class="box-value"><%= member.name %></h2>
</div>
<div class="box">
<h2 class="box-title">Created at:</h2>
<h2 class="box-value"><%= new Date(member.time).toLocaleString() %></h2>
@ -26,7 +55,6 @@
<h2 class="box-title">Is admin?</h2>
<h2 class="box-value"><%= member.admin ? "Yes" : "No" %></h2>
</div>
<div class="box">
<h2 class="box-title">Message:</h2>
<h2 class="box-value"><%= counts.message %></h2>
@ -42,71 +70,6 @@
<%= member.about %>
</p>
<% if (user?.admin && !member.deleted) {%>
<a class="btn-outline-primary" id="edit_name">Change name of the user!</a>
<a class="btn-outline-primary" id="edit_avatar">Change avatar of the user!</a>
<a class="btn-outline-primary" id="edit_about">Change about of the user!</a>
<a class="btn-outline-primary" id="admin">Give admin permissions!</a>
<a class="btn-outline-primary" id="delete">Delete user!</a>
<script type="module">
import request from "../../js/request.js";
document.addEventListener("click", async e => {
if (e.target.id == "admin") {
const response = await request("/api/users/<%= member.id %>/admin");
if (response.admin)
return alert("Making admin of " + response.name + " is success!");
} else if (e.target.id == "delete") {
const response = await request("/api/users/<%= member.id %>","DELETE");
if (!response.deleted) return
alert("User is deleted!");
location.reload()
} else {
const body = {};
if (e.target.id == "edit_name")
body.name = prompt("Enter new username!");
else if (e.target.id == "edit_avatar")
body.avatar = prompt("Enter new avatar URL!");
else if (e.target.id == "edit_about")
body.about = prompt("Enter new about text!");
else return;
const res = await request(`/api/users/<%= member.id %>`, "PATCH", body);
if (res.error) return;
alert(`User updated!`);
location.reload();
}
});
</script>
<% }; %>
<% if (member.deleted) {%>
<h1>This user has been deleted!</h1>
<a onclick="undelete();" class="btn-primary" >Undelete user! </a>
<script type="module">
import request from "../../js/request.js";
window.undelete= async function undelete(params) {
const response = await request("/api/users/<%= member.id %>/undelete");
if (response.deleted) return;
alert("User is undeleted successfully!");
location.reload()
}
</script>
<% }; %>
</div>
</body>

View file

@ -23,4 +23,3 @@
</div>
</body>