Page is OK! Edits, About me, ip ban

This commit is contained in:
Akif9748 2022-08-29 19:31:59 +03:00
parent 5b020ff548
commit 5c259f02f3
23 changed files with 4096 additions and 102 deletions

View file

@ -20,18 +20,21 @@ But in front end, the API will works with session.
- POST `/api/users/:id/delete` for delete user.
- POST `/api/users/:id/undelete` for undelete user.
- POST `/api/users/:id/admin` for give admin permissions for a user.
- POST `/api/users/:id/edit` 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.
- POST `/api/threads/:id/delete` for delete thread.
- POST `/api/threads/:id/undelete` for undelete thread.
- POST `/api/threads/:id/edit` for edit thread.
- GET `/api/messages/:id` for fetch message.
- POST `/api/messages` for create message.
- POST `/api/messages/:id/delete` for delete message.
- POST `/api/messages/:id/undelete` for undelete message.
- POST `/api/messages/:id/react/:type` for react to a message.
- POST `/api/messages/:id/edit` for edit message.
### Example request:
GET ```/api/messages/0```

View file

@ -31,17 +31,15 @@ Akf-forum has got an API for AJAX, other clients etc. And, you can learn about A
### TO-DO:
- If thread deleted, not show its messages in API.
- Profile photos will store in database
- regex for pfp for now and
- admin perm for undelete, thread + message
- page support for threads, send, if multi page, send => other page
- message "<b>"
- replacer function global
- author name of thread
- page for threads - users
- page for threads - users []
- API, ?fast=
- fix error messages, ~~declared as id~~, other...
- extra ratelimits
- better edits
### Frontend
### User
#### User
| To do | Is done? | Priority |
| ----- | -------- | -------- |
| Login via redirect query | 🟢 | HIGH |
@ -51,12 +49,12 @@ Akf-forum has got an API for AJAX, other clients etc. And, you can learn about A
| Message count | 🟢 | MEDIUM |
| Delete user | 🟢 | HIGH |
| Undelete | 🟢 | MEDIUM |
| PM | 🔴 | MEDIUM |
| About me | 🔴 | LOW |
| Edit user | 🔴 | HIGH |
| IP ban | 🔴 | MEDIUM |
| About me | 🟢 | LOW |
| Edit user | 🟡 | HIGH |
| IP ban | 🟢 | MEDIUM |
| Profile Message | 🔴 | MEDIUM |
### Messages
#### Messages
| To do | Is done? | Priority |
| ----- | -------- | -------- |
| Ratelimit | 🟢 | HIGH |
@ -65,16 +63,16 @@ Akf-forum has got an API for AJAX, other clients etc. And, you can learn about A
| Regex for scripts | 🟢 | HIGH |
| Undelete | 🟢 | MEDIUM |
| React | 🟢 | MEDIUM |
| Edit | 🔴 | MEDIUM |
| Edit | 🟢 | MEDIUM |
### Threads
#### Threads
| To do | Is done? | Priority |
| ----- | -------- | -------- |
| Ratelimit | 🟢 | HIGH |
| Create | 🟢 | HIGH |
| Delete | 🟢 | HIGH |
| Undelete | 🟢 | MEDIUM |
| Edit | 🔴 | MEDIUM |
| Edit | 🟢 | MEDIUM |
### API
| To do | Is done?
@ -85,17 +83,17 @@ Akf-forum has got an API for AJAX, other clients etc. And, you can learn about A
| Get message & thread & user | 🟢
| Delete message & thread & user | 🟢
| Undelete message & thread & user | 🟢
| Edit 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 |
| Page support, support message limit correct | 🔴 | MEDIUM |
| Locales | 🔴 | MEDIUM |
| Locales | 🔴 | LOW |
| Footer | 🔴 | LOW |
## Major Version History
- V3: New Theme

View file

@ -1,37 +1,43 @@
const { def_theme } = require("./config.json"),
session = require('express-session'),
{ UserModel } = require("./models"),
bodyParser = require('body-parser'),
port = process.env.PORT || 3000,
mongoose = require("mongoose"),
express = require('express'),
fs = require("fs"),
app = express();
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, () => console.log("Database is connected"));
mongoose.connect(process.env.MONGO_DB_URL,
async () => console.log("Connected to mongoDB with", app.ips = await BanModel.find({}).select("ip"), "banned IPs"));
app.use(session({ secret: 'secret', resave: true, saveUninitialized: true }));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(express.static("public"));
app.set("view engine", "ejs");
app.use(express.json());
app.use(async (req, res, next) => {
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);
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 });
res.error = (type, error) => res.reply("error", { type, error }, type);
if (req.user?.deleted) {
req.session.destroy();
return res.error(403, "Your account has been deleted.");
req.session.destroy();
return res.error(403, "Your account has been deleted.");
}
next();
});
}
);
for (const file of fs.readdirSync("./routes"))
app.use("/" + file.replace(".js", ""), require(`./routes/${file}`));
app.use("/" + file.replace(".js", ""), require(`./routes/${file}`));
app.all("*", (req, res) => res.error(404, "We have not got this page."));

7
models/Ban.js Normal file
View file

@ -0,0 +1,7 @@
const mongoose = require("mongoose")
const schema = new mongoose.Schema({
ip: { type: String, unique: true }
});
module.exports = mongoose.model('ban', schema);

View file

@ -14,9 +14,7 @@ const schema = new mongoose.Schema({
react: {
like: [Number],
dislike: [Number]
},
index: { type: Number, default: 0 }
}
})
schema.virtual('authorID').get(function () { return this.author?.id; });

View file

@ -1,5 +1,6 @@
const mongoose = require("mongoose")
const UserModel = require("./User");
const mongoose = require("mongoose");
const UserModel = require("./User");
const MessageModel = require("./Message");
const schema = new mongoose.Schema({
id: { type: String, unique: true },
@ -8,13 +9,19 @@ const schema = new mongoose.Schema({
title: String,
time: { type: Date, default: Date.now },
deleted: { type: Boolean, default: false },
edited: { type: Boolean, default: false },
messages: [String],
views: { type: Number, default: 0 }
});
schema.virtual('authorID').get(function() { return this.author?.id; });
schema.virtual('authorID').get(function () { return this.author?.id; });
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;

View file

@ -7,6 +7,8 @@ const schema = new mongoose.Schema({
avatar: { type: String, default: "/images/guest.png" },
time: { type: Date, default: Date.now },
deleted: { type: Boolean, default: false },
edited: { type: Boolean, default: false },
about: { type: String, default: "" },
admin: { type: Boolean, default: false },
theme: { type: String, default: def_theme }

View file

@ -1,6 +1,7 @@
const UserModel = require("./User"),
MessageModel = require("./Message"),
ThreadModel = require("./Thread"),
SecretModel = require("./Secret");
SecretModel = require("./Secret"),
BanModel = require("./Ban");
module.exports = { UserModel, MessageModel, ThreadModel, SecretModel };
module.exports = { UserModel, MessageModel, ThreadModel, SecretModel, BanModel };

3860
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -29,8 +29,11 @@
"dotenv": "^16.0.1",
"ejs": "^3.1.6",
"express": "^4.17.3",
"express-ip-block": "^0.1.2",
"express-rate-limit": "^6.5.1",
"express-session": "^1.17.2",
"mongoose": "^6.5.1"
"i": "^0.3.7",
"mongoose": "^6.5.1",
"npm": "^8.18.0"
}
}

View file

@ -1,38 +1,40 @@
import request from "./request.js";
/**
* Message Sender
*/
document.getElementById("send")?.addEventListener("submit", async e => {
window.edit_t = async function (id) {
const title = prompt("Enter new title!");
const res = await request(`/api/threads/${id}/edit`, "POST", { title });
if (res.error) return;
alert(`Thread updated`);
document.getElementById("title").innerHTML = title;
e.preventDefault();
const form = e.target;
const data = new FormData(form);
request("/api/messages", "POST", { threadID: data.get("threadID"), content: data.get("content") })
.then(res => {
if (res) location.href = `/messages/${res.id}`;
});
});
}
window.thread = async function (id, un = "") {
const res = await request(`/api/threads/${id}/${un}delete`);
if (res.error) return;
/**
* OTHER FUNCTIONS
*/
window.thread = async function (id, un= "") {
await request(`/api/threads/${id}/${un}delete`);
alert(`Thread ${un}deleted`);
location.reload();
}
window.edit_message = async function (id) {
const content = prompt("Enter new content!");
const res = await request(`/api/messages/${id}/edit`, "POST", { content });
if (res.error) return;
alert(`Message updated`);
document.getElementById("message-" + id).querySelector(".content").innerHTML = content;
}
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 = `
<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) {
const response = await request(`/api/messages/${id}/delete`);
if (response.deleted) {
@ -40,10 +42,10 @@ window.delete_message = async function (id) {
document.getElementById("dots-" + id).innerHTML = `
<i class='bx bx-trash bx-sm' id="deleted-${id}" style="color: RED;"></i>
`+ document.getElementById("dots-" + id).innerHTML;
document.getElementById("dot-" + id).innerHTML = `<a onclick="undelete_message('${id}');">UNDELETE</a>`;
}
}
window.react = async function (id, type) {
const res = await request(`/api/messages/${id}/react/${type}`)
document.getElementById(`like-${id}`).innerHTML = res.react.like.length;

View file

@ -23,7 +23,7 @@ app.use(async (req, res, next) => {
const user = await SecretModel.findOne({ username });
if (!user)
return res.error(401, "We have not got any user has got this name")
return res.error(401, `We don't have any thread with name ${username}.`)
if (!await bcrypt.compare(password, user.password)) return res.error(401, 'Incorrect Password!');

View file

@ -14,6 +14,23 @@ app.get("/:id", async (req, res) => {
res.complate(message.toObject({ virtuals: true }));
})
app.post("/:id/edit", 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}.`);
if (req.user.id !== message.authorID && !req.user.admin) return res.error(403, "You have not got permission for this.");
const { content = null } = req.body;
if (!content) return res.error(400, "Missing message content in request body.");
message.content = content;
message.edited=true;
await message.save();
res.complate(message.toObject({ virtuals: true }));
})
app.post("/", rateLimit({
@ -31,7 +48,7 @@ app.post("/", rateLimit({
if (!thread) return res.error(404, `We don't have any thread with id ${threadID}.`);
const message = await new MessageModel({ content, author: req.user, threadID: thread.id, index: thread.messages.length }).takeId();
const message = await new MessageModel({ content, author: req.user, threadID: thread.id }).takeId();
await message.save();
await thread.push(message.id).save();

View file

@ -53,7 +53,21 @@ app.post("/", async (req, res) => {
res.complate(thread.toObject({ virtuals: true }));
});
app.post("/:id/edit", async (req, res) => {
const thread = await ThreadModel.get(req.params.id);
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 (!title) return res.error(400, "Missing thread title in request body.");
thread.title = title;
await thread.save();
res.complate(thread.toObject({ virtuals: true }));
})
app.post("/:id/delete", 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}.`);
@ -77,6 +91,8 @@ app.post("/:id/undelete", async (req, res) => {
if (!thread.deleted) return res.error(404, "This thread is not deleted, first, delete it.");
thread.deleted = false;
thread.edited=true;
await thread.save();
res.complate(thread.toObject({ virtuals: true }));

View file

@ -34,7 +34,7 @@ app.post("/:id/undelete/", async (req, res) => {
const member = await UserModel.get(req.params.id);
if (!member ) return res.error(404, `We don't have any user with id ${req.params.id}.`);
if (!member) return res.error(404, `We don't have any user with id ${req.params.id}.`);
if (!member.deleted) return res.error(404, "This user is not deleted, first, delete it.");
@ -44,6 +44,31 @@ app.post("/:id/undelete/", async (req, res) => {
res.complate(member.toObject({ virtuals: true }));
})
app.post("/:id/edit", 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}.`);
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 (about) member.about = about;
member.edited = true;
await member.save();
res.complate(member.toObject({ virtuals: true }));
})
app.post("/:id/admin/", async (req, res) => {
const user = req.user;
@ -54,10 +79,10 @@ app.post("/:id/admin/", async (req, res) => {
if (!user2)
return res.error(404, `We don't have any user with id ${id}.`);
else {
user2.admin = true;
await user2.save()
}
user2.admin = true;
await user2.save()
res.complate(user2);

View file

@ -9,7 +9,7 @@ app.get("/:id", async (req, res) => {
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.redirect(`/threads/${message.threadID}?scroll=${req.params.id}`);
res.redirect(`/threads/${message.threadID}?scroll=${message.id}`);
});

View file

@ -14,7 +14,7 @@ app.post("/", async (req, res) => {
req.session.userid = null;
let { username = null, password: body_pass = null, avatar } = req.body;
let { username = null, password: body_pass = null, avatar, about } = req.body;
if (!username || !body_pass) return res.error(res, 400, "You forgot entering some values");
const user = await SecretModel.findOne({ username });
@ -24,6 +24,9 @@ app.post("/", async (req, res) => {
const user2 = new UserModel({ name: req.body.username })
if (avatar && /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g.test(avatar)) user2.avatar = avatar;
if (about) user2.about = about;
await user2.takeId()
await user2.save();

View file

@ -18,11 +18,14 @@ app.get("/:id/", async (req, res) => {
const { user, params: { id } } = req
const page = Number(req.query.page || 0);
let page = Number(req.query.page || 0);
const thread = await ThreadModel.get(id)
thread.count = await thread.messageCount(user?.admin);
thread.pages = Math.ceil(thread.count / 10);
if (thread && (user?.admin || !thread.deleted)) {
thread.views++;
const query = { threadID: id };
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)
@ -34,13 +37,12 @@ app.get("/:id/", async (req, res) => {
return message.toObject({ virtuals: true });
}))
res.reply("thread", { page, thread, messages, scroll: req.query.scroll || thread.messages[0].id });
thread.save();
} else
res.error(404, "We have not got this thread.");
res.error(404, `We don't have any thread with id ${id}.`);
});

View file

@ -18,9 +18,13 @@ app.get("/:id", async (req, res) => {
const message = await MessageModel.count({ "author.id": id });
const thread = await ThreadModel.count({ "author.id": id });
member.about = member.about.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;").replaceAll(">", "&gt;")
.replaceAll("\"", "&quot;").replaceAll("'", "&#39;")
.replaceAll("\n", "<br>");
res.reply("user", { member, counts: { message, thread } })
}
else res.error(404, `We don't have any user with id ${id}.`);
else res.error(404, `We don't have any user with id ${id}.`);
});

View file

View file

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

View file

@ -14,7 +14,7 @@
<% }; %>
<div style="text-align:center;padding:8px">
<div class="title"><%= thread.title %></div>
<div class="title" id="title"><%= thread.title %></div>
<div class="date">
<%= new Date(thread.time).toLocaleString() %> • Views: <%= thread.views %>
</div>
@ -26,7 +26,7 @@
<% if (user && !thread.deleted){ %>
<a onclick="thread('<%= thread.id %>')" class="btn-outline-primary" >DELETE</a>
<a onclick="edit_thread('<%= thread.id %>')" class="btn-outline-primary" >EDIT</a>
<a onclick="edit_t('<%= thread.id %>')" class="btn-outline-primary" >EDIT</a>
<% } else if (thread.deleted) { %>
<h3 style="display:inline;">This thread has been deleted</h3>
<a onclick="thread('<%= thread.id %>', 'un')" class="btn-primary" >UNDELETE</a>
@ -58,6 +58,9 @@
<% if (message.deleted){ %>
<i class='bx bx-trash bx-sm' id="deleted-<%=message.id %>" style="color: RED;"></i>
<% } %>
<% if (message.edited){ %>
<i class='bx bx-pencil bx-sm' id="edited-<%=message.id %>" style="color: GREEN;"></i>
<% } %>
<i class='bx bx-dots-horizontal-rounded' ></i>
</div>
@ -88,32 +91,53 @@
<% }); %>
</div>
<% if (user){ %>
<div class="message">
<form id="send">
<textarea rows="4" cols="100" name="content"></textarea>
<input name="threadID" type="hidden" value="<%= thread.id %>"></input>
<input name="page" type="hidden" value="<%= page %>"></input>
<script type="module">
import request from "../../js/request.js";
document.getElementById("send").addEventListener("submit", async e => {
e.preventDefault();
<% if (user){ %>
const data = new FormData(e.target);
request("/api/messages", "POST", { threadID: "<%= thread.id %>", content: data.get("content") })
.then(res => {
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>
<%} else {%>
<a class="btn-outline-primary" href="/login?redirect=<%= thread.getLink() %>">Login for send</a>
<% }%>
</form>
</div>
<% }%>
<div class="pagination">
<div class="back">
<% if (page > 0){ %>
<% if (page > 0){ %>
<a href="<%= thread.getLink() %>?page=<%= page-1 %>" class='bx bxs-chevron-left'></a>
<% } %>
<% } %>
</div>
<div class="numbers">
<a class="number" href="<%= thread.getLink() %>?page=<%= page %>"><%= page+1 %></a>
<% for(let i=0; i < thread.pages; i++){ %>
<a class="number <%= i==page?'active':'' %>" href="<%= thread.getLink() %>?page=<%= i %>"><%= i+1 %></a>
<% } %>
</div>
<div class="after">
<% if (Math.ceil(messages.length/10) > page){ %>
<% if (thread.pages-1 > page) { %>
<a href="<%= thread.getLink() %>?page=<%= page +1 %>" class='bx bxs-chevron-right'></a>
<% } %>
</div>

View file

@ -35,11 +35,19 @@
<h2 class="box-title">Thread:</h2>
<h2 class="box-value"><%= counts.thread %></h2>
</div>
<div class="box">
<h2 class="box-title">About:</h2><br>
</div>
<p class="box-value">
<%= member.about %>
</p>
<a class="btn-outline-primary" id="edit">Change name of the user!</a>
<% if (user?.admin && !member.deleted) {%>
<a class="btn-outline-primary" id="admin">Give admin permissions!</a>
<a class="btn-outline-primary" id="delete">Delete user!</a>
<% if (user?.admin && !member.deleted) {%>
<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";
@ -59,6 +67,15 @@
alert("User is deleted!");
location.reload()
}else if (e.target.id == "edit") {
const name = prompt("Enter new username!");
const res =await request(`/api/users/<%= member.id %>/edit`, "POST", { name });
if (res.error) return;
alert(`User updated!`);
location.reload();
}