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. - DELETE `/api/users/:id/` for delete user.
- PATCH `/api/users/:id/` for edit user. - PATCH `/api/users/:id/` for edit user.
- POST `/api/users/:id/undelete` for undelete 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. - GET `/api/threads/:id` for fetch thread.
- DELETE `/api/threads/:id/` for delete 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... Edit `config.json` for default themes of users...
## API ## 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 ## Credits
* [Akif9748](https://github.com/Akif9748) - Project mainteiner, main developer, made **old** frontend * [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 | | To do | Is done? | Priority |
| ----- | -------- | -------- | | ----- | -------- | -------- |
| Profile Message | 🔴 | LOW | | 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 | | Search | 🔴 | MEDIUM |
| Footer | 🟢 | LOW | | Footer | 🟡 | LOW |
- Profile photos will store in database - Better Auth
- Profile photos will store in a folder
- replacer function global - replacer function global
- author name of thread
- page for threads - users - page for threads - users
- extra ratelimits - IPs of users will add SecretModel
- better edits
- IP BAN fix, user -> ips []
- message counts for API - message counts for API
- ZATEN SİLİNDİ BU KİŞİ & MESAJ - better theme patch UserModel
- delete admin request, moreover, add it to user patch delete 😳, better theme patch - ajax, delete update thread dom
### API ### API
| To do | Is done? | To do | Is done?

View file

@ -8,6 +8,7 @@ const { UserModel, BanModel } = require("./models"),
express = require('express'), express = require('express'),
fs = require("fs"), fs = require("fs"),
app = express(); app = express();
const rateLimit = require('express-rate-limit')
app.ips = []; app.ips = [];
@ -17,10 +18,11 @@ mongoose.connect(process.env.MONGO_DB_URL,
app.set("view engine", "ejs"); app.set("view engine", "ejs");
app.use(session({ secret: 'secret', resave: true, saveUninitialized: true }), app.use(
bodyParser.urlencoded({ extended: true }), session({ secret: 'secret', resave: true, saveUninitialized: true }),
express.static("public"), express.json(), ipBlock(app.ips), express.static("public"), express.json(), ipBlock(app.ips),
async (req, res, next) => { async (req, res, next) => {
req.headers["x-forwarded-for"]
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) res.reply = (page, options = {}, status = 200) => res.status(status)
.render(page, { user: req.user, theme: req.user?.theme || def_theme, ...options }); .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."); return res.error(403, "Your account has been deleted.");
} }
next(); 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")) for (const file of fs.readdirSync("./routes"))

View file

@ -2,7 +2,7 @@ const mongoose = require("mongoose")
const schema = new mongoose.Schema({ const schema = new mongoose.Schema({
username: { type: String, unique: true }, username: { type: String, unique: true },
password: String, password: String, ips: [String],
id: { type: String, unique: true } 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 Query */
@media(max-width:980px) { @media(max-width:980px) {

View file

@ -1,5 +1,7 @@
import request from "./request.js"; import request from "./request.js";
// THREAD:
window.edit_thread = async function (id) { window.edit_thread = async function (id) {
const title = prompt("Enter new title!"); const title = prompt("Enter new title!");
const res = await request(`/api/threads/${id}/`, "PATCH", { 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) { window.delete_thread = async function (id) {
const res = await request(`/api/threads/${id}/`, "DELETE"); const res = await request(`/api/threads/${id}/`, "DELETE");
if (res.error) return; if (res.error) return;
alert(`Thread deleted`); alert(`Thread deleted`);
location.reload(); location.reload();
} }
@ -19,41 +20,56 @@ window.delete_thread = async function (id) {
window.undelete_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}/undelete`);
if (res.error) return; if (res.error) return;
alert(`Thread undeleted`); alert(`Thread undeleted`);
location.reload(); 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`); 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) { window.undelete_message = async function (id) {
const response = await request(`/api/messages/${id}/undelete`); const response = await request(`/api/messages/${id}/undelete`);
if (response.deleted) return; if (response.deleted) return;
document.getElementById("deleted-" + id).remove(); const message = document.getElementById("message-" + id);
document.getElementById("dot-" + id).innerHTML = `
message.querySelector("#deleted").remove();
message.querySelector(".dots-menu").innerHTML = `
<a onclick="delete_message('${id}');">DELETE</a> <a onclick="delete_message('${id}');">DELETE</a>
<a onclick="edit_message('${id}');">EDIT</a>` <a onclick="edit_message('${id}');">EDIT</a>`
} }
window.delete_message = async function (id) { window.delete_message = async function (id) {
const response = await request(`/api/messages/${id}/`, "DELETE"); const response = await request(`/api/messages/${id}/`, "DELETE");
if (response.deleted) { if (!response.deleted) return
const message = document.getElementById(`message-${id}`);
alert("Message deleted"); alert("Message deleted");
document.getElementById("dots-" + id).innerHTML = `
<i class='bx bx-trash bx-sm' id="deleted-${id}" style="color: var(--important)"></i> message.querySelector(".dots-menu").innerHTML = `<a onclick="undelete_message('${id}');">UNDELETE</a>`;
`+ document.getElementById("dots-" + id).innerHTML;
document.getElementById("dot-" + id).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) { window.react = async function (id, type) {
const res = await request(`/api/messages/${id}/react/${type}`) const res = await request(`/api/messages/${id}/react/${type}`)
document.getElementById(`like-${id}`).innerHTML = res.react.like.length; const message = document.getElementById(`message-${id}`);
document.getElementById(`dislike-${id}`).innerHTML = res.react.dislike.length; for (const react in res.react)
message.querySelector("#" + react).innerHTML = res.react[react].length;
} }

View file

@ -10,7 +10,7 @@ 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) 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}.`) return res.error(404, `You do not have permissions to view this message with id ${id}.`)
next(); next();
@ -97,6 +97,7 @@ app.delete("/:id/", async (req, res) => {
if (user.id != message.authorID && !user.admin) if (user.id != message.authorID && !user.admin)
return res.error(403, "You have not got permission for this."); 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; message.deleted = true;
await message.save(); await message.save();

View file

@ -70,6 +70,7 @@ app.delete("/:id/", async (req, res) => {
if (user.id != thread.authorID && !user.admin) if (user.id != thread.authorID && !user.admin)
return res.error(403, "You have not got permission for this."); 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; thread.deleted = true;
await thread.save(); await thread.save();
console.log(thread) console.log(thread)

View file

@ -14,16 +14,10 @@ app.param("id", async (req, res, next, id) => {
next(); next();
}); });
app.get("/:id", async (req, res) => { app.get("/:id", async (req, res) => res.complate(req.member));
if (req.member.not()) return;
res.complate(member);
});
app.delete("/:id/", async (req, res) => { app.delete("/:id/", async (req, res) => {
const { user, member } = req; const { user, member } = req;
if (req.member.not()) return;
if (!user.admin) if (!user.admin)
return res.error(403, "You have not got permission for this."); 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) => { app.patch("/:id/", async (req, res) => {
const { user, member } = req; const { user, member } = req;
if (req.user.id !== member.id && !req.user.admin) return res.error(403, "You have not got permission for this."); if (req.user.id !== member.id && !user.admin) return res.error(403, "You have not got permission for this.");
const { avatar, name, about, theme } = req.body; if (!Object.values(req.body).some(Boolean)) return res.error(400, "Missing member informations in request body.");
if (!avatar && !name && !about && !theme) 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)) 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; member.avatar = avatar;
if (name) { if (name) {
await SecretModel.findOneAndUpdate({ name: member.name }, { name }); await SecretModel.findOneAndUpdate({ name: member.name }, { name });
member.name = name; member.name = name;
} }
if (about) member.about = about; if (about) member.about = about;
if (theme) member.theme = member.theme === "default" ? "black" : "default"; if (theme)
member.theme = theme; member.theme = member.theme === "default" ? "black" : "default";
if(typeof admin === "boolean" || ["false","true"].includes(admin)) member.admin = admin;
member.edited = true; member.edited = true;
await member.save(); 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; module.exports = app;

View file

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

View file

@ -8,11 +8,10 @@ const app = Router();
app.get("/", (req, res) => res.reply("register", { user: null })); app.get("/", (req, res) => res.reply("register", { user: null }));
app.post("/", rateLimit({ 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") handler: (_r, response, _n, options) => response.error(options.statusCode, "You are begin ratelimited")
}), async (req, res) => { }), async (req, res) => {
req.session.userID = null; req.session.destroy()
let { username = null, password: body_pass = null, avatar, about } = req.body; 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) => { app.get("/:id", async (req, res) => {
const user = req.user const user = req.user
const { id } = req.params; const { id } = req.params;

View file

@ -13,7 +13,8 @@
width: 100%; width: 100%;
} }
td, th { td,
th {
border: 1px solid #dddddd; border: 1px solid #dddddd;
text-align: left; text-align: left;
padding: 8px; padding: 8px;
@ -33,7 +34,7 @@
<h2 style="color: var(--second);">Banned users:</h2> <h2 style="color: var(--second);">Banned users:</h2>
<table > <table>
<tr> <tr>
<th>IP</th> <th>IP</th>
<th>Reason</th> <th>Reason</th>
@ -52,19 +53,19 @@
<script type="module"> <script type="module">
import request from "../../js/request.js"; import request from "../../js/request.js";
window.unban = async function () { window.unban = async function() {
const ip = prompt("Enter ip to unban"); const ip = prompt("Enter ip to unban");
const response = await request("/api/bans/"+ip,"DELETE"); const response = await request("/api/bans/" + ip, "DELETE");
if(response) if (response)
alert("IP unbanned!"); alert("IP unbanned!");
else else
alert("IP is not unbanned!"); alert("IP is not unbanned!");
location.reload(); location.reload();
} }
window.ban = async function () { window.ban = async function() {
const ip = prompt("Enter ip to ban"); const ip = prompt("Enter ip to ban");
const response = await request("/api/bans/"+ip); const response = await request("/api/bans/" + ip);
if(response) if (response)
alert("IP banned!"); alert("IP banned!");
else else
alert("IP is not banned!"); alert("IP is not banned!");
@ -74,4 +75,5 @@
</div> </div>
</body> </body>
</html> </html>

View file

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

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@
<%- include("extra/meta", {title: "Welcome to the Akf-forum!" }) %> <%- include("extra/meta", {title: "Welcome to the Akf-forum!" }) %>
<body > <body>
<link rel="stylesheet" href="/css/user.css" /> <link rel="stylesheet" href="/css/user.css" />
<%- include("extra/navbar") %> <%- include("extra/navbar") %>
@ -13,7 +13,8 @@
<div class="content"> <div class="content">
<% if (user) { %> <% 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 class="avatar"><img src="<%=user.avatar %>"></div>
</div> </div>
@ -58,4 +59,3 @@
</body> </body>
</html> </html>

View file

@ -1,4 +1,3 @@
</html> </html>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
@ -16,7 +15,7 @@
<input type="text" name="username" placeholder="Username" class="input" required> <input type="text" name="username" placeholder="Username" class="input" required>
<input type="password" name="password" placeholder="Password" class="input" required> <input type="password" name="password" placeholder="Password" class="input" required>
<input type="submit" style="width:100%"class="btn-primary" value="Login"> <input type="submit" style="width:100%" class="btn-primary" value="Login">
</form> </form>

View file

@ -17,10 +17,8 @@
<input type="text" name="username" placeholder="Username" class="input" required> <input type="text" name="username" placeholder="Username" class="input" required>
<input type="password" name="password" placeholder="Password" 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="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="text" name="avatar" placeholder="Avatar URL (not required)" class="input">
<input type="submit" class="btn-primary" style="width:100%;" value="Register"> <input type="submit" class="btn-primary" style="width:100%;" value="Register">
</form> </form>

View file

@ -16,31 +16,33 @@
<div style="text-align:center;padding:8px"> <div style="text-align:center;padding:8px">
<div class="title" id="title"><%= thread.title %></div> <div class="title" id="title"><%= thread.title %></div>
<div class="date"> <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> </div>
<div style="text-align:center;padding:8px"> <div style="text-align:center;padding:8px">
<!-- THREAD AUTHOR AND PROFILE PHOTO --> <!-- THREAD AUTHOR AND PROFILE PHOTO -->
<% if (user && !thread.deleted){ %> <% if (user && !thread.deleted){ %>
<a onclick="delete_thread('<%= thread.id %>')" class="btn-outline-primary" >DELETE</a> <a onclick="delete_thread('<%= thread.id %>')" class="btn-outline-primary">DELETE</a>
<a onclick="edit_thread('<%= thread.id %>')" class="btn-outline-primary" >EDIT</a> <a onclick="edit_thread('<%= thread.id %>')" class="btn-outline-primary">EDIT</a>
<% } else if (thread.deleted) { %> <% } else if (thread.deleted) { %>
<h3 style="display:inline;">This thread has been deleted</h3> <h3 style="display:inline;">This thread has been deleted</h3>
<a onclick="undelete_thread('<%= thread.id %>')" class="btn-primary" >UNDELETE</a> <a onclick="undelete_thread('<%= thread.id %>')" class="btn-primary">UNDELETE</a>
<% }; %> <% }; %>
</div> </div>
<div id="messages" value="<%= thread.id %>"> <div id="messages">
<% messages.filter(Boolean).forEach(message=>{ %> <% messages.filter(Boolean).forEach(message=>{ %>
<div class="message" id="message-<%= message.id %>"> <div class="message" id="message-<%= message.id %>">
<div class="left"> <div class="left">
<img src="<%= message.author.avatar || '/images/guest.png' %>"/> <img src="<%= message.author.avatar || '/images/guest.png' %>" />
<div class="username"><a href="/users/<%=message.authorID %>"><%=message.author.name %></a></div> <div class="username"><a href="/users/<%=message.authorID %>"><%=message.author.name %></a></div>
<div class="date"> <div class="date">
<%= new Date(message.time).toLocaleDateString() %> <%= new Date(message.time).toLocaleDateString() %>
@ -54,17 +56,17 @@
<% if(user){ %> <% if(user){ %>
<% if(user.id === message.authorID || user.admin){ %> <% 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){ %> <% 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){ %> <% 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> <i class='bx bx-dots-horizontal-rounded'></i>
</div> </div>
<div class="dots-menu" id="dot-<%=message.id %>"> <div class="dots-menu">
<% if (!message.deleted){ %> <% if (!message.deleted){ %>
<a onclick="delete_message('<%=message.id %>');">Delete</a> <a onclick="delete_message('<%=message.id %>');">Delete</a>
<a onclick="edit_message('<%=message.id %>');">Edit</a> <a onclick="edit_message('<%=message.id %>');">Edit</a>
@ -74,15 +76,14 @@
</div> </div>
<% } %> <% } %>
<div class="reactions"> <div class="reactions">
<div> <div>
<i onclick='react("<%= message.id %>","like");' class='bx bx-like'></i> <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>
<div> <div>
<i onclick='react("<%= message.id %>","dislike");' class='bx bx-dislike'></i> <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>
</div> </div>
<% }; %> <% }; %>
@ -93,17 +94,16 @@
</div> </div>
<% if (user){ %> <% if (user){ %>
<div class="message"> <div class="message" id="send-div">
<form id="send" style="width:100%"> <form id="send" style="width:100%">
<textarea rows="4" <textarea rows="4" name="content"></textarea>
style="
font-family:inherit;
width: 100%;
margin: 10px;
border: 2px solid #e3e3e3;" name="content"></textarea>
<input name="threadID" type="hidden" value="<%= thread.id %>"></input> <input name="threadID" type="hidden" value="<%= thread.id %>"></input>
<input name="page" type="hidden" value="<%= page %>"></input> <input name="page" type="hidden" value="<%= page %>"></input>
<button class="btn-primary">Send!</button>
</form>
</div>
<script type="module"> <script type="module">
import request from "../../js/request.js"; import request from "../../js/request.js";
@ -113,20 +113,19 @@
const data = new FormData(e.target); const data = new FormData(e.target);
request("/api/messages", "POST", { threadID: "<%= thread.id %>", content: data.get("content") }) const res = await request("/api/messages", "POST", {
.then(res => { threadID: "<%= thread.id %>",
content: data.get("content")
})
let tp = Number("<%= thread.pages %>") let tp = Number("<%= thread.pages %>")
let tm = Number("<%= thread.count %>") let tm = Number("<%= thread.count %>")
if (tp*10===tm) tp++; if (tp * 10 === tm) tp++;
if (res) location.href = `/threads/${data.get("threadID")}?page=${tp-1}`; if (res) location.href = `/threads/${data.get("threadID")}?page=${tp-1}`;
});
}); });
</script> </script>
<button class="btn-primary">Send!</button>
</form>
</div>
<% }%> <% }%>
<div class="pagination"> <div class="pagination">
<div class="back"> <div class="back">
@ -151,8 +150,9 @@
<script> <script>
document.getElementById("message-<%= scroll %>").scrollIntoView(); document.getElementById("message-<%= scroll %>").scrollIntoView();
function dots(id) { function dots(id) {
document.getElementById('dot-'+id).classList.toggle('active') document.getElementById('message-' + id).querySelector(".dots-menu").classList.toggle('active')
} }
</script> </script>

View file

@ -3,13 +3,13 @@
<%- include("extra/meta", {title: "Thread list!" }) %> <%- include("extra/meta", {title: "Thread list!" }) %>
<body> <body>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css" integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A==" crossorigin="anonymous" referrerpolicy="no-referrer" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css" integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="/css/threads.css" /> <link rel="stylesheet" href="/css/threads.css" />
<%- include("extra/navbar") %> <%- include("extra/navbar") %>
<div class="threads"> <div class="threads">
<% threads.forEach(thread=>{ %> <% threads.forEach(thread=>{ %>
<a href="<%= thread.getLink() %>" class=""> <a href="<%= thread.getLink() %>" class="">
@ -31,6 +31,6 @@
<br> <br>
<% }); %> <% }); %>
</div> </div>
</body> </body>

View file

@ -6,10 +6,40 @@
<body> <body>
<link rel="stylesheet" href="/css/user.css" /> <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") %> <%- include("extra/navbar") %>
<div class="content"> <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;"> <div class="box" style="justify-content:center;">
<img style="width:100px;height:100px;border-radius:50%;" src="<%=member.avatar %>"> <img style="width:100px;height:100px;border-radius:50%;" src="<%=member.avatar %>">
</div> </div>
@ -17,7 +47,6 @@
<h2 class="box-title">Name:</h2> <h2 class="box-title">Name:</h2>
<h2 class="box-value"><%= member.name %></h2> <h2 class="box-value"><%= member.name %></h2>
</div> </div>
<div class="box"> <div class="box">
<h2 class="box-title">Created at:</h2> <h2 class="box-title">Created at:</h2>
<h2 class="box-value"><%= new Date(member.time).toLocaleString() %></h2> <h2 class="box-value"><%= new Date(member.time).toLocaleString() %></h2>
@ -26,9 +55,8 @@
<h2 class="box-title">Is admin?</h2> <h2 class="box-title">Is admin?</h2>
<h2 class="box-value"><%= member.admin ? "Yes" : "No" %></h2> <h2 class="box-value"><%= member.admin ? "Yes" : "No" %></h2>
</div> </div>
<div class="box"> <div class="box">
<h2 class="box-title"> Message:</h2> <h2 class="box-title">Message:</h2>
<h2 class="box-value"><%= counts.message %></h2> <h2 class="box-value"><%= counts.message %></h2>
</div> </div>
<div class="box"> <div class="box">
@ -42,71 +70,6 @@
<%= member.about %> <%= member.about %>
</p> </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> </div>
</body> </body>

View file

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