Compare commits
30 Commits
Author | SHA1 | Date |
---|---|---|
Akif9748 | 8c20c70874 | |
Akif9748 | 9b5bb9b782 | |
Akif9748 | 0438e4fdb4 | |
Akif9748 | 00ea899fb4 | |
Akif9748 | aace4c6fd4 | |
Akif9748 | d4021e0e4a | |
Akif9748 | 29f8af1394 | |
Akif9748 | bda7988000 | |
Akif9748 | f6cafd17b9 | |
Akif9748 | 9ea41d8372 | |
Akif9748 | f2da949114 | |
Akif9748 | ef8cb5e819 | |
Akif9748 | 6b983f9e55 | |
Akif9748 | 4811c7a0e3 | |
Akif9748 | fed6ea0668 | |
Akif9748 | fe550051a7 | |
Akif9748 | 31b9249cf8 | |
Akif9748 | 138b585482 | |
Akif9748 | f31003b3f2 | |
Akif9748 | 0bdc21ff35 | |
Akif9748 | 9598813e66 | |
Akif9748 | ccd04139ba | |
Akif9748 | 8923293198 | |
Akif9748 | 9c183eb05e | |
Akif9748 | 0311f363e2 | |
Akif9748 | 8fe083ded0 | |
Akif9748 | 922b8e5732 | |
Akif9748 | 9ee1d589eb | |
Akif9748 | 83ae8507c0 | |
Akif9748 | e3b6345196 |
|
@ -11,5 +11,5 @@ config.json
|
|||
test.js
|
||||
|
||||
# avatars folder
|
||||
public/images/avatars/*
|
||||
!public/images/avatars/default.jpg
|
||||
src/public/images/avatars/*
|
||||
!src/public/images/avatars/default.jpg
|
81
README.md
81
README.md
|
@ -40,58 +40,65 @@ Akf-forum has got an API for AJAX (fetch), other clients etc. And, you can learn
|
|||
|
||||
## Screenshot
|
||||
|
||||
### Thread Page w/Black Theme
|
||||
![black-theme](https://user-images.githubusercontent.com/70021050/187899782-2ff010aa-0d39-4fc2-b00c-19bcf1623c8a.png)
|
||||
### Threads Page w/Default Theme
|
||||
![light-theme](https://user-images.githubusercontent.com/70021050/186941146-f9a8fbf8-9b2b-4028-afc8-81cff559d9fb.png)
|
||||
<details>
|
||||
<summary><b>Mobile view</b></summary>
|
||||
<img src="https://user-images.githubusercontent.com/70021050/187901065-fd75ef85-56e3-42ce-8b34-cb8d799a6517.png"></img>
|
||||
</details>
|
||||
### Thread Page w/Bootstrap theme
|
||||
![image](https://github.com/Akif9748/akf-forum/assets/70021050/1ad4ad8e-d000-46a6-834e-7d76cdddda60)
|
||||
|
||||
## TO-DO list
|
||||
|
||||
### Backend:
|
||||
#### Feature:
|
||||
- Profile Message or DM
|
||||
- mod role, permissions
|
||||
- upload other photos, model for it
|
||||
- change password.
|
||||
- add approval threads page.
|
||||
- Upload other photos, model for it
|
||||
- Edit & download template
|
||||
- Banner
|
||||
- Add @me support for ids, <%= member.id %>
|
||||
- Roles & Permissions
|
||||
```
|
||||
role: "moderator",
|
||||
permissions: ["see_deleted_message"]
|
||||
```
|
||||
|
||||
#### Fixes:
|
||||
- Admin deleting other admins.
|
||||
- theme.js, change theme js code to a file.
|
||||
- same email discord???? direct err
|
||||
- IMPORTANT: add user/member id to file so scripts can access
|
||||
|
||||
#### ETC:
|
||||
- Rewrite apidocs
|
||||
- Add a feature list to README.md
|
||||
- delete admin???
|
||||
|
||||
### Frontend
|
||||
#### Features:
|
||||
- change category name
|
||||
- _id
|
||||
- add support for transition around gravatar
|
||||
- BETTER SETUP PAGE
|
||||
- add used open source libraries to README.md
|
||||
- send public to common/public
|
||||
- new screenshoots
|
||||
- user.ejs for per theme
|
||||
### front-end
|
||||
- text alling center body
|
||||
- add a css file for CodeMirror in threads / send message ok
|
||||
- Add approval threads page.
|
||||
- add support for switch around gravatar and upload photo
|
||||
- old contents / titles add to forum interface
|
||||
- categories page is need a update, thread count in category (?)
|
||||
- add ban button to user profile
|
||||
- who liked a message
|
||||
- give admin button, not is admin
|
||||
- edit user ++
|
||||
- rewrite main page, list new messages
|
||||
|
||||
#### Fixes:
|
||||
- BETTER SETUP PAGE: use setup everytime
|
||||
- add threads, messages etc. to "extra" folder
|
||||
- add category to thread list page
|
||||
- working reset button
|
||||
- text alling center body
|
||||
- thread.js unfuction only listener
|
||||
- show error on modal
|
||||
- send delete, ban to user settings (edit user) menu and fix edit user menu
|
||||
|
||||
## Special Thanks:
|
||||
https://github.com/akashgiricse/Online-Forum for bootstrap theme.
|
||||
|
||||
#### css fix
|
||||
- admin page to css file
|
||||
threads:
|
||||
<style>
|
||||
.fa {
|
||||
color: var(--main);
|
||||
}
|
||||
</style>
|
||||
@Tokmak for old frontend.
|
||||
|
||||
https://fengyuanchen.github.io/cropperjs/examples/crop-a-round-image.html for avatar upload panel.
|
||||
|
||||
https://github.com/mdbootstrap/bootstrap-profiles for profile page of bootstrap theme.
|
||||
|
||||
## Major Version History
|
||||
- V5: Enchanted Themes
|
||||
- V4: Caching
|
||||
- V3: New Theme
|
||||
- V2: Backend fix, mongoose is fixed. Really big fix.
|
||||
- V1: Mongoose added.
|
||||
- V0: Birth with quick.db
|
||||
- V0: Birth with quick.db
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
"codename": "bootstrap_black",
|
||||
"language": "en"
|
||||
},
|
||||
"forum_name": "akf",
|
||||
"description": "Akf-forum!",
|
||||
"forum_name": "akf-forum",
|
||||
"description": "Akf-forum offical site!",
|
||||
"limits": {
|
||||
"title": 128,
|
||||
"message": 1024,
|
||||
|
@ -13,7 +13,7 @@
|
|||
},
|
||||
"global_ratelimit": {
|
||||
"enabled": true,
|
||||
"max": 25,
|
||||
"max": 30,
|
||||
"windowMs": 60000
|
||||
},
|
||||
"discord_auth": false,
|
||||
|
|
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
|
@ -1,10 +1,12 @@
|
|||
{
|
||||
"name": "akf-forum",
|
||||
"version": "5.2.5",
|
||||
"version": "5.7.0",
|
||||
"description": "A Node.js based forum software",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"start": "node .",
|
||||
"reset": "node util/reset",
|
||||
"admin": "node util/admin",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -21,24 +23,23 @@
|
|||
"url": "https://github.com/Akif9748/akf-forum/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
"node": ">=18 >=17.5 >=16.15"
|
||||
},
|
||||
"homepage": "https://akf-forum.glitch.me/",
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.0",
|
||||
"connect-mongo": "^4.6.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"ejs": "^3.1.6",
|
||||
"bcrypt": "^5.1.1",
|
||||
"connect-mongo": "^5.1.0",
|
||||
"dotenv": "^16.4.2",
|
||||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^6.6.0",
|
||||
"express-session": "^1.17.2",
|
||||
"mongoose": "^6.6.5",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"express-session": "^1.18.0",
|
||||
"mongoose": "^8.1.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"nodemailer": "^6.8.0",
|
||||
"nodemailer": "^6.9.9",
|
||||
"request-ip": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.25.0"
|
||||
"eslint": "^8.56.0"
|
||||
}
|
||||
}
|
||||
|
|
29
src/index.js
29
src/index.js
|
@ -20,33 +20,42 @@ const
|
|||
|
||||
app.ips = [];
|
||||
|
||||
app.onlines = new Map();
|
||||
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [ip, lastSeen] of app.onlines.entries())
|
||||
if (now - lastSeen > 1000 * 60 * 5)
|
||||
app.onlines.delete(ip);
|
||||
}, 1000 * 60 * 5);
|
||||
|
||||
app.set("view engine", "ejs");
|
||||
app.set("limits", limits);
|
||||
|
||||
if (RLS.enabled) app.use(RL(RLS.windowMs, RLS.max));
|
||||
|
||||
for (const theme of fs.readdirSync(join(__dirname, "themes")))
|
||||
app.use(`/themes/${theme}`, express.static(join(__dirname, "themes", theme, "public")));
|
||||
|
||||
for (const theme of themes)
|
||||
app.use(`/themes/${theme.codename}`, express.static(join(__dirname, "themes", theme.codename, "public")));
|
||||
|
||||
app.use(express.static(join(__dirname, "public")), express.json(), express.urlencoded({ extended: true }), IP(),
|
||||
SES({ secret: process.env.SECRET, store: MS.create({ clientPromise: DB, stringify: false }), resave: false, saveUninitialized: false }),
|
||||
async (req, res, next) => {
|
||||
if (app.ips.includes(req.clientIp)) return res.status(403).send("You are banned from this forum.");
|
||||
|
||||
|
||||
const lastSeen = Date.now();
|
||||
|
||||
req.user = req.session.userID ? await UserModel.findOneAndUpdate({ id: req.session.userID }, {
|
||||
lastSeen: Date.now(), $addToSet: { ips: req.clientIp }
|
||||
lastSeen, $addToSet: { ips: req.clientIp }
|
||||
}) : null;
|
||||
|
||||
app.onlines.set(req.clientIp, lastSeen);
|
||||
|
||||
let theme = req.user?.theme || def_theme;
|
||||
|
||||
if (!themes.some(t => t.codename === theme.codename))
|
||||
theme = def_theme;
|
||||
|
||||
|
||||
res.reply = (page, options = {}, status = 200) => {
|
||||
const road = join(__dirname, "themes", theme.codename, "views", `${page}.ejs`);
|
||||
const renderpage = fs.existsSync(road) ? road : join(__dirname, "themes", "common", "views", `${page}.ejs`);
|
||||
const renderpage = fs.existsSync(road) ? road : join(__dirname, "themes", def_theme, "views", `${page}.ejs`);
|
||||
return res.status(status).render(renderpage, {
|
||||
dataset: {
|
||||
themes, theme, forum_name, description,
|
||||
|
@ -70,6 +79,8 @@ app.use(express.static(join(__dirname, "public")), express.json(), express.urlen
|
|||
}
|
||||
);
|
||||
|
||||
if (RLS.enabled) app.use(RL(RLS.windowMs, RLS.max));
|
||||
|
||||
if (discord_auth)
|
||||
app.set("DISCORD_AUTH_URL", `https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_ID}&redirect_uri=${host}%2Fauth%2Fdiscord&response_type=code&scope=identify`);
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ const { readdirSync } = require('fs');
|
|||
const { join } = require('path');
|
||||
require("dotenv").config();
|
||||
module.exports = {
|
||||
themes: readdirSync(join(__dirname, "themes")).filter(f => f !== "common").map(f => require(`./themes/${f}`)),
|
||||
themes: readdirSync(join(__dirname, "themes")).map(f => require(`./themes/${f}`)),
|
||||
threadEnum: ["OPEN", "APPROVAL", "DELETED"],
|
||||
userEnum: ["ACTIVE", "APPROVAL", "DELETED", "BANNED"],
|
||||
RL(windowMs = 60_000, max = 1) {
|
||||
|
|
|
@ -7,8 +7,10 @@ const schema = new mongoose.Schema({
|
|||
authorID: { type: String }
|
||||
|
||||
});
|
||||
schema.methods.takeId = async function () {
|
||||
this.id = String(await model.count() || 0);
|
||||
|
||||
schema.methods.takeId = async function () {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
this.id = await model.countDocuments() || 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -27,8 +27,9 @@ const schema = new mongoose.Schema({
|
|||
|
||||
schema.methods.get_author = cache.getAuthor
|
||||
|
||||
schema.methods.takeId = async function () {
|
||||
this.id = String(await model.count() || 0);
|
||||
schema.methods.takeId = async function () {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
this.id = await model.countDocuments() || 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ schema.methods.get_category = async function () {
|
|||
schema.methods.messageCount = async function (admin = false) {
|
||||
const query = { threadID: this.id };
|
||||
if (!admin) query.deleted = false;
|
||||
return await MessageModel.count(query) || 0;
|
||||
return await MessageModel.countDocuments(query) || 0;
|
||||
};
|
||||
|
||||
schema.methods.push = function (messageID) {
|
||||
|
@ -51,8 +51,9 @@ schema.methods.push = function (messageID) {
|
|||
return this;
|
||||
}
|
||||
|
||||
schema.methods.takeId = async function () {
|
||||
this.id = await model.count() || 0;
|
||||
schema.methods.takeId = async function () {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
this.id = await model.countDocuments() || 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,8 @@ schema.virtual("active").get(function () {
|
|||
})
|
||||
|
||||
schema.methods.takeId = async function () {
|
||||
this.id = String(await model.count() || 0);
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
this.id = String(await model.countDocuments() || 0);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
const UserModel = require("./User");
|
||||
const UserCache = [];
|
||||
|
||||
const UserCache = new Map();
|
||||
module.exports.getAuthor = async function () {
|
||||
const id = this.authorID || this.author?.id;
|
||||
let user = UserCache.find(user => user?.id == id)
|
||||
let user = UserCache.get(id);
|
||||
if (!user)
|
||||
UserCache.push(user = await UserModel.findOne({ id }))
|
||||
UserCache.set(id, user = await UserModel.findOne({ id }));
|
||||
|
||||
if (!this.get('authorID', null, { getters: false })) {
|
||||
this.authorID = user.id;
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 704 KiB After Width: | Height: | Size: 206 KiB |
|
@ -0,0 +1,63 @@
|
|||
function b64toBlob(b64Data, contentType = "", sliceSize = 512) {
|
||||
|
||||
const byteCharacters = atob(b64Data);
|
||||
const byteArrays = [];
|
||||
|
||||
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
||||
const slice = byteCharacters.slice(offset, offset + sliceSize);
|
||||
|
||||
const byteNumbers = new Array(slice.length);
|
||||
for (let i = 0; i < slice.length; i++)
|
||||
byteNumbers[i] = slice.charCodeAt(i);
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
byteArrays.push(byteArray);
|
||||
}
|
||||
const blob = new Blob(byteArrays, {
|
||||
type: contentType
|
||||
});
|
||||
return blob;
|
||||
}
|
||||
|
||||
const image = document.getElementById('image');
|
||||
const button = document.getElementById('button');
|
||||
const reader = new FileReader();
|
||||
|
||||
document.getElementById("file-input")
|
||||
.addEventListener("change", function () {
|
||||
reader.onload = () => {
|
||||
image.src = reader.result;
|
||||
|
||||
let croppable = false;
|
||||
// eslint-disable-next-line no-undef
|
||||
const cropper = new Cropper(image, {
|
||||
aspectRatio: 1,
|
||||
viewMode: 1,
|
||||
ready: function () {
|
||||
croppable = true;
|
||||
},
|
||||
});
|
||||
|
||||
button.onclick = async () => {
|
||||
if (!croppable) return;
|
||||
|
||||
const croppedCanvas = cropper.getCroppedCanvas();
|
||||
// const croppedImage = document.getElementById('cropped-image');
|
||||
const body = new FormData(document.createElement('form'))
|
||||
const block = croppedCanvas.toDataURL().split(";");
|
||||
const contentType = block[0].split(":")[1];
|
||||
const realData = block[1].split(",")[1];
|
||||
body.append('avatar', b64toBlob(realData, contentType));
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const res = await fetch('/api/users/'+MEM_ID+'/avatar', {
|
||||
method: 'POST',
|
||||
body
|
||||
}).then(res => res.json());
|
||||
|
||||
if (res.error) return alert(res.error);
|
||||
alert('Success!');
|
||||
location.reload();
|
||||
};
|
||||
}
|
||||
reader.readAsDataURL(event.target.files[0]);
|
||||
});
|
File diff suppressed because one or more lines are too long
|
@ -1,16 +1,30 @@
|
|||
const { UserModel, ThreadModel, MessageModel, CategoryModel } = require("../models")
|
||||
const { UserModel, ThreadModel, MessageModel, CategoryModel } = require("../models");
|
||||
const { Router } = require("express");
|
||||
const app = Router();
|
||||
|
||||
app.get("/", async (req, res) => {
|
||||
|
||||
const categories = await CategoryModel.count(),
|
||||
users = await UserModel.count({ deleted: false }),
|
||||
threads = await ThreadModel.count({ state: "OPEN" }),
|
||||
messages = await MessageModel.count({ deleted: false }),
|
||||
newestMember = await UserModel.findOne({ deleted: false }, "name").sort({ time: -1 });
|
||||
const [
|
||||
categories, users, threads, messages, newestMember, newestMessages, newestThreads, onlineMemberCount, onlineMembers
|
||||
] = await Promise.all([
|
||||
CategoryModel.countDocuments(),
|
||||
UserModel.countDocuments({ deleted: false }),
|
||||
ThreadModel.countDocuments({ state: "OPEN" }),
|
||||
MessageModel.countDocuments({ deleted: false }),
|
||||
UserModel.findOne({ deleted: false }, "name id").sort({ time: -1 }),
|
||||
MessageModel.find({ deleted: false }).sort({ time: -1 }).limit(10),
|
||||
ThreadModel.find({ state: "OPEN" }).sort({ time: -1 }).limit(10),
|
||||
UserModel.countDocuments({ deleted: false, lastSeen: { $gt: Date.now() - 1000 * 60 * 5 } }),
|
||||
UserModel.find({ deleted: false, hideLastSeen: false, lastSeen: { $gt: Date.now() - 1000 * 60 * 5 } }, "name id")
|
||||
]),
|
||||
onlineTotal = req.app.onlines.size,
|
||||
onlineGuests = onlineTotal - onlineMemberCount;
|
||||
|
||||
res.reply("index", { categories, users, threads, messages, newestMember: newestMember.name });
|
||||
res.reply("index", {
|
||||
categories, users, threads, messages,
|
||||
newestMember, newestMessages, newestThreads,
|
||||
onlineMemberCount, onlineMembers, onlineGuests, onlineTotal
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
const { UserModel, BanModel } = require("../../../models");
|
||||
const { Router } = require("express");
|
||||
const multer = require("multer");
|
||||
const { themes } = require("../../../lib")
|
||||
const { themes, emailRegEx } = require("../../../lib")
|
||||
const app = Router();
|
||||
const { join } = require("path");
|
||||
app.param("id", async (req, res, next, id) => {
|
||||
if (id === "me") //
|
||||
id = req.user.id;
|
||||
|
||||
req.member = await UserModel.get(id, req.user.admin ? "+lastSeen +ips" : "");
|
||||
|
||||
if (!req.member) return res.error(404, `We don't have any user with id ${id}.`);
|
||||
|
@ -37,7 +40,7 @@ app.patch("/:id", async (req, res) => {
|
|||
if (req.user.id !== member.id && !user.admin) return res.error(403, "You have not got permission for this.");
|
||||
if (!Object.keys(req.body).some(Boolean)) return res.error(400, "Missing member informations in request body.");
|
||||
|
||||
const { name, about, admin, deleted, hideLastSeen, theme } = req.body;
|
||||
const { name, about, admin, deleted, hideLastSeen, theme, email } = req.body;
|
||||
|
||||
if ((admin?.length || "deleted" in req.body) && !req.user.admin) return res.error(403, "You have not got permission for edit 'admin' and 'deleted' information, or bad request.");
|
||||
const { names, desp } = req.app.get("limits");
|
||||
|
@ -54,10 +57,16 @@ app.patch("/:id", async (req, res) => {
|
|||
if (theme && themes.some(t => t.codename === theme.codename))
|
||||
member.theme = theme;
|
||||
|
||||
if (email) {
|
||||
if (!emailRegEx.test(email)) return res.error(400, "E-mail is not valid");
|
||||
if (await UserModel.exists({ email })) return res.error(400, "E-mail is already in use");
|
||||
member.email = email;
|
||||
}
|
||||
|
||||
if (typeof admin === "boolean" || ["false", "true"].includes(admin)) member.admin = admin;
|
||||
if (deleted === false) member.deleted = false;
|
||||
|
||||
if (typeof hideLastSeen === "boolean") member.hideLastSeen = hideLastSeen;
|
||||
if (typeof hideLastSeen === "boolean" || ["false", "true"].includes(admin)) member.hideLastSeen = hideLastSeen;
|
||||
|
||||
member.edited = true;
|
||||
|
||||
|
@ -95,7 +104,7 @@ app.post("/:id/avatar", upload.single('avatar'), async (req, res) => {
|
|||
if (req.user.id !== member.id && !req.user.admin) return res.error(403, "You have not got permission for this.");
|
||||
|
||||
if (!req.file) return res.error(400, "Missing avatar in request body.");
|
||||
member.avatar = req.file.destination.slice("./public".length) + "/" + req.file.filename;
|
||||
member.avatar = "/images/avatars/" + req.file.filename;
|
||||
res.complate(await member.save());
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
const { Router } = require("express")
|
||||
const { UserModel } = require("../models");
|
||||
const fetch = require("node-fetch");
|
||||
const app = Router();
|
||||
const { host, email_auth } = require("../../config.json")
|
||||
|
||||
|
@ -17,7 +16,7 @@ app.get("/discord", async (req, res) => {
|
|||
client_secret: process.env.DISCORD_SECRET,
|
||||
grant_type: 'authorization_code',
|
||||
redirect_uri: host + "/auth/discord",
|
||||
scope: 'identify',
|
||||
scope: 'identify+email',
|
||||
}).toString(),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
|
@ -53,13 +52,14 @@ app.get("/discord", async (req, res) => {
|
|||
return res.redirect("/");
|
||||
}
|
||||
|
||||
let name = discord.username + discord.discriminator;
|
||||
while (await UserModel.findOne({ name }))
|
||||
let name = discord.username;
|
||||
while (await UserModel.exists({ name }))
|
||||
name += Math.floor(Math.random() * 2);
|
||||
|
||||
const user2 = new UserModel({
|
||||
name, discordID: discord.id, discord_code: code,
|
||||
avatar: `https://cdn.discordapp.com/avatars/${discord.id}/${discord.avatar}.png?size=256`
|
||||
name, email: discord.email,
|
||||
discordID: discord.id, discord_code: code,
|
||||
avatar: `https://cdn.discordapp.com/avatars/${discord.id}/${discord.avatar}.png?size=256`,
|
||||
});
|
||||
|
||||
await user2.takeId();
|
||||
|
|
|
@ -9,7 +9,7 @@ app.get("/", async (req, res) => {
|
|||
const categories = await CategoryModel.find({}).limit(10).skip(page * 10).sort({ time: -1 });
|
||||
res.reply("categories", {
|
||||
categories, page,
|
||||
pages: Math.ceil(await CategoryModel.count({}) / 10)
|
||||
pages: Math.ceil(await CategoryModel.countDocuments({}) / 10)
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -24,7 +24,7 @@ app.get("/:id", async (req, res) => {
|
|||
let threads = await ThreadModel.find(query).limit(10).skip(page * 10).sort({ time: -1 });
|
||||
threads = await Promise.all(threads.map(thread => thread.get_author()));
|
||||
|
||||
res.reply("threads", { threads, page, title: `Threads in ${category.name}`, desp: category.desp, pages: Math.ceil(await ThreadModel.count(query) / 10) });
|
||||
res.reply("threads", { threads, page, title: `Threads in ${category.name}`, desp: category.desp, pages: Math.ceil(await ThreadModel.countDocuments(query) / 10) });
|
||||
});
|
||||
|
||||
module.exports = app;
|
|
@ -43,7 +43,7 @@ app.post("/", RL(24 * 60 * 60_000, 5), async (req, res) => {
|
|||
to: email,
|
||||
subject: name + ", please verify your email",
|
||||
html: `
|
||||
<h1>Verify your email in ${forum_name}-forum</h1>
|
||||
<h1>Verify your email in ${forum_name}</h1>
|
||||
<a href="${host}/auth/email?code=${user.email_code}">Click here to verify your email</a>
|
||||
`
|
||||
}, (err) => {
|
||||
|
|
|
@ -19,7 +19,7 @@ app.get("/users", async (req, res) => {
|
|||
const users = await UserModel.find({ ...req.sq, name: { $regex: req.query.q, $options: "i" } }, null, req.so)
|
||||
res.reply("users", {
|
||||
users, page: req.page,
|
||||
pages: Math.ceil(await UserModel.count(req.sq) / 10)
|
||||
pages: Math.ceil(await UserModel.countDocuments(req.sq) / 10)
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -31,7 +31,7 @@ app.get("/messages", async (req, res) => {
|
|||
const messages = await MessageModel.find(query, null, req.so)
|
||||
res.reply("messages", {
|
||||
messages, page: req.page,
|
||||
pages: Math.ceil(await MessageModel.count(query) / 10)
|
||||
pages: Math.ceil(await MessageModel.countDocuments(query) / 10)
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -45,7 +45,7 @@ app.get("/threads", async (req, res) => {
|
|||
const threads = await ThreadModel.find(query, null, req.so)
|
||||
res.reply("threads", {
|
||||
threads, page: req.page, title: `Threads with query '${req.query.q}'`,
|
||||
pages: Math.ceil(await ThreadModel.count(query) / 10), desp: `${threads.length} threads are listed`
|
||||
pages: Math.ceil(await ThreadModel.countDocuments(query) / 10), desp: `${threads.length} threads are listed`
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
const { UserModel } = require("../models");
|
||||
const { Router } = require("express")
|
||||
const bcrypt = require("bcrypt");
|
||||
const { RL} = require('../lib');
|
||||
const app = Router();
|
||||
|
||||
app.use(async (req, res, next) => {
|
||||
if (!req.user) return res.error(403, "You are not logged in");
|
||||
next();
|
||||
});
|
||||
|
||||
app.get("/", (req, res) => res.reply("security"));
|
||||
|
||||
app.post("/", RL(24 * 60 * 60_000, 5), async (req, res) => {
|
||||
|
||||
let { old_password, password } = req.body;
|
||||
if (!old_password || !password) return res.error(400, "You forgot entering some values");
|
||||
const { names } = req.app.get("limits");
|
||||
if (password.length < 3 || password.length > names) return res.error(400, "Password must be between 3 - 25 characters");
|
||||
const user = await UserModel.get(req.user.id, "+password");
|
||||
if (!await bcrypt.compare(old_password, user.password)) return res.error(401, 'Incorrect password!');
|
||||
user.password = await bcrypt.hash(password, 10);
|
||||
await user.save();
|
||||
res.send("Password changed");
|
||||
|
||||
});
|
||||
|
||||
module.exports = app;
|
|
@ -8,7 +8,7 @@ app.get("/", async (req, res) => {
|
|||
let threads = await ThreadModel.find(query).limit(10).skip(page * 10).sort({ time: -1 });
|
||||
threads = await Promise.all(threads.map(thread => thread.get_author()));
|
||||
|
||||
return res.reply("threads", { threads, page, title: "Threads", desp: threads.length + " threads are listed", pages: Math.ceil(await ThreadModel.count(query) / 10) });
|
||||
return res.reply("threads", { threads, page, title: "Threads", desp: threads.length + " threads are listed", pages: Math.ceil(await ThreadModel.countDocuments(query) / 10) });
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ app.get("/", async (req, res) => {
|
|||
const page = Number(req.query.page) || 0;
|
||||
const query = req.user?.admin ? {} : { deleted: false };
|
||||
let users = await UserModel.find(query).limit(10).skip(page * 10);
|
||||
return res.reply("users", { users, page, pages: Math.ceil(await UserModel.count(query) / 10) });
|
||||
return res.reply("users", { users, page, pages: Math.ceil(await UserModel.countDocuments(query) / 10) });
|
||||
});
|
||||
|
||||
app.get("/:id/avatar", async (req, res) => {
|
||||
|
@ -26,8 +26,8 @@ app.get("/:id", async (req, res) => {
|
|||
|
||||
if (member && (user?.admin || !member.deleted)) {
|
||||
|
||||
const message = await MessageModel.count({ authorID: id });
|
||||
const thread = await ThreadModel.count({ authorID: id });
|
||||
const message = await MessageModel.countDocuments({ authorID: id });
|
||||
const thread = await ThreadModel.countDocuments({ authorID: id });
|
||||
res.reply("user", { member, counts: { message, thread }, discord: req.app.get("DISCORD_AUTH_URL") })
|
||||
}
|
||||
else res.error(404, `We don't have any user with id ${id}.`);
|
||||
|
|
|
@ -293,51 +293,6 @@ a {
|
|||
}
|
||||
|
||||
|
||||
/***********************************
|
||||
PAGINATION
|
||||
***********************************/
|
||||
.pagination {
|
||||
box-shadow: 0 0 5px 0 var(--box-shadow);
|
||||
margin: 10px auto;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 400px;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pagination .back,
|
||||
.pagination .after {
|
||||
color: var(--second);
|
||||
font-size: 26px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.pagination .numbers {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.pagination .number {
|
||||
color: var(--second);
|
||||
font-size: 22px;
|
||||
border: 0 0 5px var(--second);
|
||||
padding: 8px;
|
||||
border-radius: 2px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.pagination .number.active {
|
||||
color: var(--main);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
||||
/************************************
|
||||
Threads
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
|
@ -0,0 +1,124 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Admin Panel!" }) %>
|
||||
|
||||
|
||||
<body style="text-align: center;">
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<style>
|
||||
table {
|
||||
font-family: arial, sans-serif;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border: 1px solid #dddddd;
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
color: var(--anti);
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: #dddddd;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h2>Welcome to the admin panel of the forum, <%= user.name %>!</h1>
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="exampleModalLabel">Banned users</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" onclick="document.getElementById('exampleModal').style.display = 'none';">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<table>
|
||||
<tr>
|
||||
<th>IP</th>
|
||||
<th>Reason</th>
|
||||
<th>AuthorID</th>
|
||||
</tr>
|
||||
<% for (const ban of bans) { %>
|
||||
<tr>
|
||||
<td><%=ban.ip%></td>
|
||||
<td><%=ban.reason%></td>
|
||||
<td><%=ban.authorID%></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a class="btn-primary" onclick="ban();">IP BAN</a>
|
||||
<a class="btn-outline-primary" onclick="unban();">REMOVE IP BAN</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<button onclick="window.location.href = '/categories/create';" class="btn-primary">Create Category</button>
|
||||
<button onclick="window.location.href = '/admin/config';" class="btn-primary">Edit config</button>
|
||||
|
||||
<button onclick="document.getElementById('exampleModal').style.display = 'block';" class="btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#exampleModal" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
|
||||
Banned users
|
||||
</button>
|
||||
<div>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Admin list</th>
|
||||
</tr>
|
||||
<% for (const admin of admins) { %>
|
||||
<tr>
|
||||
<td><a style="color: var(--anti);" href="<%= admin.getLink() %>"><%= admin.name %></a></td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</table>
|
||||
<ul>
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import request from "../../js/request.js";
|
||||
|
||||
window.unban = async function() {
|
||||
const ip = prompt("Enter ip to unban");
|
||||
if (!ip) return;
|
||||
const response = await request("/api/bans/" + ip, "DELETE");
|
||||
if (response)
|
||||
alert("IP unbanned!");
|
||||
else
|
||||
alert("IP is not unbanned!");
|
||||
location.reload();
|
||||
}
|
||||
window.ban = async function() {
|
||||
const ip = prompt("Enter ip to ban");
|
||||
if (!ip) return;
|
||||
|
||||
const response = await request("/api/bans/" + ip);
|
||||
if (response)
|
||||
alert("IP banned!");
|
||||
else
|
||||
alert("IP is not banned!");
|
||||
location.reload();
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,55 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Avatar Upload Panel!" }) %>
|
||||
|
||||
<body style="text-align: center;">
|
||||
<link rel="stylesheet" href="/libs/cropper/cropper.css">
|
||||
<style>
|
||||
.container {
|
||||
margin: 20px auto;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.cropper-view-box,
|
||||
.cropper-face {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.cropper-view-box {
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 1px #39f;
|
||||
}
|
||||
</style>
|
||||
<script src="/libs/cropper/cropper.js"></script>
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
|
||||
<div class="container">
|
||||
<h1>Upload avatar for <%= member.name %></h1>
|
||||
|
||||
<input type="file" id="file-input">
|
||||
<img id="image" src="<%= member.avatar %>">
|
||||
<p>
|
||||
<button type="button" id="button">Upload</button>
|
||||
</p>
|
||||
<file id="cropped-image" name="avatar">
|
||||
<form id="form"></form>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
const MEM_ID="<%= member.id %>";
|
||||
</script>
|
||||
<script src="/js/avatar.js"></script>
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,76 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Category list!" }) %>
|
||||
|
||||
<body>
|
||||
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<div class="container my-3">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="h4 text-white bg-info mb-0 p-4 rounded-top"><%= "Categories" %></h2>
|
||||
|
||||
<table class="table table-striped table-bordered table-responsive-lg">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th scope="col" class="topic-col">Topic</th>
|
||||
<th scope="col">Description</th>
|
||||
<% if (user?.admin){ %> <th scope="col" class="last-post-col">Action</th> <% } %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% categories.forEach(category=>{ %>
|
||||
<tr>
|
||||
<td>
|
||||
<h3 class="h6">
|
||||
<a href="<%= category.getLink() %>"><%= category.name %></a>
|
||||
</h3>
|
||||
</td>
|
||||
<td>
|
||||
<div><%= category.desp %></div>
|
||||
</td>
|
||||
<% if (user?.admin){ %>
|
||||
<td>
|
||||
<a class="btn-danger" onclick="fetch('/api/categories/<%= category.id %>/',{method:'DELETE'})"><i class="bx bx-trash bx-sm"></i></a>
|
||||
</td>
|
||||
<% } %>
|
||||
</tr>
|
||||
<% }); %>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<% if(typeof page === "number"){ %>
|
||||
<div class="mb-3 clearfix">
|
||||
<nav aria-label="Navigate post pages">
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class="page-item <%= page > 0 ?"": "disabled" %>">
|
||||
<a class="page-link" href="/categories?page=<%= page-1 %>" tabindex="-1">Back</a>
|
||||
</li>
|
||||
<% for(let i=0; i < pages; i++){ %>
|
||||
<li class="page-item">
|
||||
<a class="page-link <%= i==page?'active':'' %>" href="/categories?page=<%= i %>"><%= i+1 %>
|
||||
<% if (i==page){ %>
|
||||
<span class="sr-only">(current)</span>
|
||||
<% } %>
|
||||
</a>
|
||||
</li>
|
||||
<% } %>
|
||||
<li class="page-item <%= pages-1 > page ?"": "disabled" %>">
|
||||
<a class="page-link" href="/categories?page=<%= page+1 %>">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<% } %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,59 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Create Category!" }) %>
|
||||
|
||||
<body>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<link rel="stylesheet" href="/libs/simplemde/simplemde.min.css">
|
||||
<script src="/libs/simplemde/simplemde.min.js"></script>
|
||||
|
||||
<div class="container my-3">
|
||||
<div class="col-12">
|
||||
<h2 class="h4 text-white bg-info mb-3 p-4 rounded">Create new category</h2>
|
||||
<form class="mb-3">
|
||||
<div class="form-group">
|
||||
<label for="topic">Name</label>
|
||||
<input type="text" class="form-control" id="title" placeholder="Give your category a name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="comment">Description</label>
|
||||
<textarea id="textarea"></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Create</button>
|
||||
<button type="reset" class="btn btn-danger">Reset</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script src="/js/editor.js"></script>
|
||||
|
||||
|
||||
<script type="module">
|
||||
const simplemde = editor("category-create");
|
||||
|
||||
import request from "../../js/request.js";
|
||||
|
||||
document.addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
const response = await request("/api/categories/", "POST", {
|
||||
name: document.getElementById("title").value,
|
||||
desp: simplemde.value()
|
||||
});
|
||||
|
||||
simplemde.clearAutosavedValue();
|
||||
|
||||
if (response)
|
||||
window.location.href = "/categories/" + response.id;
|
||||
|
||||
|
||||
});
|
||||
</script>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,69 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Create thread!" }) %>
|
||||
|
||||
<body>
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<link rel="stylesheet" href="/libs/simplemde/simplemde.min.css">
|
||||
<script src="/libs/simplemde/simplemde.min.js"></script>
|
||||
|
||||
|
||||
<div class="container my-3">
|
||||
<div class="col-12">
|
||||
<h2 class="h4 text-white bg-info mb-3 p-4 rounded">Create new thread</h2>
|
||||
<form class="mb-3">
|
||||
<div class="form-group">
|
||||
<label for="topic">Title</label>
|
||||
<input type="text" class="form-control" id="title" placeholder="Give your thread a title." required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="comment">Comment:</label>
|
||||
<textarea id="textarea"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-check-label">
|
||||
Category:
|
||||
</label>
|
||||
|
||||
<select id="category" class="input">
|
||||
<% for (const category of categories) { %>
|
||||
<option value="<%= category.id %>"><%= category.name %></option>
|
||||
<% } %>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Create</button>
|
||||
<button type="reset" class="btn btn-danger">Reset</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/editor.js"></script>
|
||||
|
||||
|
||||
<script type="module">
|
||||
const simplemde = editor("thread-create");
|
||||
|
||||
import request from "../../js/request.js";
|
||||
|
||||
document.addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
const response = await request("/api/threads/", "POST", {
|
||||
title: document.getElementById("title").value,
|
||||
content: simplemde.value(),
|
||||
category: document.getElementById("category").value
|
||||
});
|
||||
if (response) {
|
||||
simplemde.clearAutosavedValue();
|
||||
window.location.href = "/threads/" + response.id;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,62 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: member.name }) %>
|
||||
|
||||
|
||||
<body>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<link rel="stylesheet" href="/libs/simplemde/simplemde.min.css">
|
||||
<script src="/libs/simplemde/simplemde.min.js"></script>
|
||||
|
||||
<div class="container my-3">
|
||||
<div class="col-12">
|
||||
<a class="btn-primary" href="/security" >Change your own password</a>
|
||||
|
||||
<h2 class="h4 text-white bg-info mb-3 p-4 rounded">Edit User</h2>
|
||||
<form id="form" class="mb-3">
|
||||
<input type="text" name="name" placeholder="<%= member.name %>" class="input">
|
||||
<input type="email" name="email" placeholder="<%= member.email %>" class="input">
|
||||
<textarea id="textarea" class="input" name="about" rows="4" cols="60" 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'>
|
||||
<% } %>
|
||||
Hide Last Seen? <input id='last' type='checkbox' value='true' name='hideLastSeen' <%=member.hideLastSeen ? "checked": ""%>>
|
||||
<input id='lastHidden' type='hidden' value='false' name='admin'>
|
||||
<button type="submit" class="btn-primary" style="width:100%;">Update User!</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/editor.js"></script>
|
||||
|
||||
<script type="module">
|
||||
import request from "/js/request.js";
|
||||
const simplemde = editor("user-edit-<%=member.id%>");
|
||||
|
||||
document.getElementById("form").addEventListener("submit", async e => {
|
||||
e.preventDefault();
|
||||
document.getElementById('adminHidden').disabled = document.getElementById("admin").checked;
|
||||
document.getElementById('lastHidden').disabled = document.getElementById("last").checked;
|
||||
|
||||
const formdata = new FormData(e.target)
|
||||
const res = await request("/api/users/<%=member.id%>", "PATCH", {
|
||||
name: formdata.get("name"),
|
||||
about: simplemde.value(),
|
||||
admin: formdata.get("admin"),
|
||||
email: formdata.get("email"),
|
||||
hideLastSeen: formdata.get("hideLastSeen")
|
||||
});
|
||||
simplemde.clearAutosavedValue();
|
||||
|
||||
if (res) alert(`User is updated!`);
|
||||
location.reload();
|
||||
});
|
||||
</script>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,7 +1,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title><%= title || dataset.forum_name +"-forum" %></title>
|
||||
<title><%= title || dataset.forum_name %></title>
|
||||
<meta name="description" content="<%= dataset.description %>">
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
<link href="/themes/bootstrap_black/bootstrap-night.min.css" rel="stylesheet">
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<a class="navbar-brand" href="/"><%= dataset.forum_name.toUpperCase() %><span>-FORUM</span></a>
|
||||
<a class="navbar-brand" href="/"><%= dataset.forum_name.toUpperCase() %></a>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarText">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include("extra/meta", {title: "Welcome to the "+dataset.forum_name+"-forum!" }) %>
|
||||
<%- include("extra/meta", {title: "Welcome to the " + dataset.forum_name }) %>
|
||||
|
||||
|
||||
<body>
|
||||
<%- include("extra/navbar") %>
|
||||
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
|
||||
|
||||
|
||||
<div class="container my-3">
|
||||
|
@ -14,46 +15,87 @@
|
|||
<% if (user) { %>
|
||||
Welcome, <%= user.name %>
|
||||
<% } else { %>
|
||||
Welcome, Guest! <a href="/register">You can press to register.</a>
|
||||
Welcome, Guest! <a href="/register">You can press here to register.</a>
|
||||
<% } %>
|
||||
</span>
|
||||
</nav>
|
||||
<div class="row">
|
||||
<div class="col-12 col-xl-9">
|
||||
|
||||
<div class="category">
|
||||
|
||||
<h2 class="h4 text-white bg-danger mb-0 p-4 rounded-top">Forum category</h2>
|
||||
<h2 class="h4 text-white bg-danger mb-0 p-4 rounded-top">New threads</h2>
|
||||
<table class="table table-striped table-bordered table-responsive">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th scope="col" class="forum-col">Forum</th>
|
||||
<th scope="col">Topics</th>
|
||||
<th scope="col">Posts</th>
|
||||
<th scope="col" class="last-post-col">Last post</th>
|
||||
<th scope="col" class="topic-col">Topic</th>
|
||||
<th scope="col" class="created-col">Created</th>
|
||||
<th scope="col">Statistics</th>
|
||||
<% if (user?.admin){ %> <th scope="col" class="last-post-col">Action</th> <% } %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="thread">
|
||||
<% newestThreads.forEach(thread=>{ %>
|
||||
<tr>
|
||||
<td>
|
||||
<h3 class="h5 mb-0"><a href="#0" class="text-uppercase">Forum name</a></h3>
|
||||
<p class="mb-0">Lorem ipsum dolor sit amet, consectetur adipiscing elit. In laoreet pellentesque lorem sed elementum.</p>
|
||||
<h3 class="h6">
|
||||
<% if (thread.deleted) { %> <span class="badge badge-primary">[DELETED]</span><% } %>
|
||||
<a href="<%= thread.getLink() %>"><%= thread.title %></a>
|
||||
</h3>
|
||||
</td>
|
||||
<td>
|
||||
<div>5</div>
|
||||
<div class="avatar">by <a href="/users/<%= thread.authorID %>"><%= thread.author.name %></a><img src="<%=thread.author.avatar %>"></div>
|
||||
<div><%= new Date(thread.time).toLocaleString() %></div>
|
||||
</td>
|
||||
<td>
|
||||
<div>18</div>
|
||||
<div><%= thread.messages.length %> messages</div>
|
||||
<div><%= thread.views %> views</div>
|
||||
</td>
|
||||
<% if (user?.admin){ %>
|
||||
<td>
|
||||
<% if (!thread.deleted){ %>
|
||||
<a class="btn-danger" onclick="fetch('/api/threads/<%= thread.id %>/',{method:'DELETE'})"><i class="bx bx-trash bx-sm"></i></a>
|
||||
<% } %>
|
||||
</td>
|
||||
<% } %>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="category">
|
||||
|
||||
<h2 class="h4 text-white bg-danger mb-0 p-4 rounded-top">New messages</h2>
|
||||
<table class="table table-striped table-bordered table-responsive">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th scope="col" class="forum-col">Message</th>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col" class="last-post-col">Author</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% newestMessages.forEach(message => { %>
|
||||
<tr>
|
||||
<td>
|
||||
<p class="mb-0">
|
||||
<a href="<%= message.getLink() %>"> <%= message.content.slice(0, 100) %></a>
|
||||
</p>
|
||||
</td>
|
||||
<td>
|
||||
<h4 class="h6 mb-0 font-weight-bold"><a href="#0">Post name</a></h4>
|
||||
<div>by <a href="#0">Author name</a></div>
|
||||
<div>05 Apr 2017, 20:07</div>
|
||||
<div><%= new Date(message.time).toLocaleString() %></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="avatar">by <a href="<%= message.getLink() %>"><%= message.author.name %></a><img src="<%=message.author.avatar %>"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-12 col-xl-3">
|
||||
<aside>
|
||||
<div class="row">
|
||||
|
@ -61,23 +103,36 @@
|
|||
<div class="card mb-3 mb-sm-0 mb-xl-3">
|
||||
<div class="card-body">
|
||||
<h2 class="h4 card-title">Members online</h2>
|
||||
<style>
|
||||
li.d-inline~li.d-inline::before {
|
||||
content: ', ';
|
||||
}
|
||||
</style>
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li><a href="/users">You</a></li>
|
||||
|
||||
<% onlineMembers.slice(0, 30).forEach(user => { %>
|
||||
<li class="d-inline">
|
||||
<a href="/users/<%=user.id %>"><%= user.name %></a>
|
||||
</li>
|
||||
<% }); %>
|
||||
<% if (onlineMemberCount > 30) { %>
|
||||
<li>
|
||||
<a href="/users/"><%= onlineMemberCount - 30 %></a> more...
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-8">Total:</dt>
|
||||
<dd class="col-4 mb-0">-</dd>
|
||||
<dd class="col-4 mb-0"><%= onlineTotal %></dd>
|
||||
</dl>
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-8">Members:</dt>
|
||||
<dd class="col-4 mb-0">-</dd>
|
||||
<dd class="col-4 mb-0"><%= onlineMemberCount %></dd>
|
||||
</dl>
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-8">Guests:</dt>
|
||||
<dd class="col-4 mb-0">-</dd>
|
||||
<dd class="col-4 mb-0"><%= onlineGuests %></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -85,11 +140,9 @@
|
|||
<div class="col-12 col-sm-6 col-xl-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
|
||||
<h2 class="h4 card-title">Forum statistics</h2>
|
||||
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-8">Total forums:</dt>
|
||||
<dt class="col-8">Total categories:</dt>
|
||||
<dd class="col-4 mb-0"><%= categories %></dd>
|
||||
</dl>
|
||||
<dl class="row mb-0">
|
||||
|
@ -104,11 +157,13 @@
|
|||
<dt class="col-8">Total members:</dt>
|
||||
<dd class="col-4 mb-0"><%= users %></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<% if(newestMember) {%>
|
||||
<div class="card-footer">
|
||||
<div>Newest member:</div>
|
||||
<div><a href="#0"><%= newestMember %></a></div>
|
||||
<div><a href="/users/<%= newestMember.id %>"><%= newestMember.name %></a></div>
|
||||
</div>
|
||||
<% }%>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Log in!" }) %>
|
||||
|
||||
<body style="text-align: center;">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<div class="jumbotron jumbotron-fluid">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-sm-8 col-md-6 col-lg-4 offset-sm-4 offset-sd-3 offset-lg-4">
|
||||
<h1 class="mb-3 text-center">Please log in</h1>
|
||||
<form action="/login?redirect=<%= redirect !== "/register" ? redirect : "/" %>" method="post" class="mb-3">
|
||||
<div class="form-group">
|
||||
<label for="name">Email/Username:</label>
|
||||
<input name="name" class="form-control" placeholder="example@email.com" id="email" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" class="form-control" id="password" required />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
<div class="text-center">
|
||||
<p>or..</p>
|
||||
<a href="/register" class="btn btn-outline-primary">Create account</a>
|
||||
|
||||
<% if(discord) { %>
|
||||
<a href="<%=discord%>" class="btn btn-outline-primary">Auth with discord</a>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,23 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Change Password!" }) %>
|
||||
|
||||
<body style="text-align: center;">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
<h1 class="title">Change password</h1>
|
||||
|
||||
<form class="login" action="/security" method="post">
|
||||
<input type="password" name="old_password" placeholder="Old Password" class="input" required>
|
||||
<input type="password" name="password" placeholder="Password" class="input" required>
|
||||
<input type="submit" style="width:100%" class="btn-primary" value="Change it">
|
||||
</form>
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -11,15 +11,14 @@
|
|||
<h1 style="color: var(--main);">Setup</h1>
|
||||
<h2 style="color: var(--second);">There is default settings for akf-forum, you not need to edit them, but you can! And, the first registered user will be admin.</h2>
|
||||
<form class="search" method="POST">
|
||||
|
||||
Forum name:
|
||||
<input class="input" type="text" name="forum_name" value="akf" required>
|
||||
<input class="input" type="text" name="forum_name" value="akf-forum" required>
|
||||
Forum description:
|
||||
<input class="input" type="text" name="description" value="Akf-forum!" required>
|
||||
Default state for new threads, change with "APPROVAL" for approval system:
|
||||
<input class="input" type="text" name="default_thread_state" value="OPEN" required>
|
||||
Default state for new users, change with "APPROVAL" for approval system:
|
||||
<input class="input" type="text" name="default_thread_state" value="ACTIVE" required>
|
||||
<input class="input" type="text" name="default_user_state" value="ACTIVE" required>
|
||||
Domain of the forum, defaulty setted:
|
||||
<input class="input" type="text" name="host" id="domain" value="Akf-forum!" required>
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: thread.title }) %>
|
||||
|
||||
|
||||
<body>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<script src="/libs/showdown/showdown.min.js"></script>
|
||||
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
|
||||
<link rel="stylesheet" href="/libs/simplemde/simplemde.min.css">
|
||||
<script src="/libs/simplemde/simplemde.min.js"></script>
|
||||
|
||||
<div style="text-align:center;padding:8px">
|
||||
<a href="/categories/<%= thread.categoryID %>" class="title" id="title"><%= thread.title %></a>
|
||||
<div class="date">
|
||||
<%= new Date(thread.time).toLocaleString() %> • Views: <%= thread.views %>
|
||||
</div>
|
||||
<div class="date">
|
||||
<a style="color: var(--anti);" href="/users/<%= thread.author.id %>"><%= thread.author.name %></a> <%= "• "+(thread.edited ? "Edited" : "Not edited")%>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div style="text-align:center;padding:8px">
|
||||
<% if (user && (user.id === thread.authorID || user.admin ) && !thread.deleted){ %>
|
||||
<a onclick="delete_thread('<%= thread.id %>')" class="btn-outline-primary">DELETE</a>
|
||||
<a onclick="edit_thread('<%= thread.id %>')" class="btn-outline-primary">EDIT</a>
|
||||
<% } else if (thread.deleted) { %>
|
||||
<h4 class="title" style="display:inline; font-size: 20px;">This thread has been deleted</h3>
|
||||
<a onclick="undelete_thread('<%= thread.id %>')" class="btn-primary">UNDELETE</a>
|
||||
<% }; %>
|
||||
</div>
|
||||
|
||||
<div id="messages">
|
||||
|
||||
<% messages.filter(Boolean).forEach(message=>{ %>
|
||||
|
||||
<div class="message" id="message-<%= message.id %>">
|
||||
<div class="left">
|
||||
<img src="<%= message.author.avatar %>" />
|
||||
<div class="username"><a href="/users/<%=message.author.id %>"><%=message.author.name %></a></div>
|
||||
<div class="date">
|
||||
<%= new Date(message.time).toLocaleDateString() %>
|
||||
</div>
|
||||
<div class="date">
|
||||
<%= new Date(message.time).toLocaleTimeString() %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content"><%= message.content %></div>
|
||||
<% if(user){ %>
|
||||
<% if(user.id === message.authorID || user.admin){ %>
|
||||
|
||||
<div class="dots" modal="#modal-<%=message.id %>">
|
||||
<% if (message.deleted){ %>
|
||||
<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" style="color: GREEN;"></i>
|
||||
<% } %>
|
||||
<i class='bx bx-dots-horizontal-rounded'></i>
|
||||
</div>
|
||||
|
||||
<div class="dots-menu" id="modal-<%=message.id %>">
|
||||
<% if (!message.deleted){ %>
|
||||
<a onclick="delete_message('<%=message.id %>');">Delete</a>
|
||||
<a onclick="edit_message('<%=message.id %>');">Edit</a>
|
||||
<% }else { %>
|
||||
<a onclick="undelete_message('<%=message.id %>');">Undelete</a>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="reactions">
|
||||
<div <% if (message.react.like.includes(user?.id)) { %> style="color: var(--main)" <% } %>>
|
||||
<i onclick='react("<%= message.id %>","like");' class='bx bx-like'></i>
|
||||
<div id="like"><%=message.react.like.length %></div>
|
||||
</div>
|
||||
<div <% if (message.react.dislike.includes(user?.id)) { %> style="color: var(--main)" <% } %>>
|
||||
<i onclick='react("<%= message.id %>","dislike");' class='bx bx-dislike'></i>
|
||||
<div id="dislike"><%=message.react.dislike.length %></div>
|
||||
</div>
|
||||
</div>
|
||||
<% }; %>
|
||||
|
||||
</div>
|
||||
|
||||
<% }); %>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const converter = new showdown.Converter();
|
||||
for (const message of document.querySelectorAll(".message")) {
|
||||
const content = message.querySelector(".content");
|
||||
content.innerHTML = converter.makeHtml(content.rawText = content.innerHTML);
|
||||
}
|
||||
</script>
|
||||
<script src="/js/modal.js"></script>
|
||||
<% if (user){ %>
|
||||
<script type="module" src="/js/thread.js"></script>
|
||||
|
||||
<div class="message" id="send-div">
|
||||
|
||||
<form id="send" style="width:100%">
|
||||
<textarea rows="4" id="textarea"></textarea>
|
||||
<input name="page" type="hidden" value="<%= page %>"></input>
|
||||
<button class="btn-primary">Send!</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/js/editor.js"></script>
|
||||
|
||||
<script type="module">
|
||||
const simplemde = editor("thread-<%= thread.id %>");
|
||||
|
||||
import request from "../../js/request.js";
|
||||
|
||||
document.getElementById("send").addEventListener("submit", async e => {
|
||||
|
||||
e.preventDefault();
|
||||
const res = await request("/api/messages", "POST", {
|
||||
threadID: "<%= thread.id %>",
|
||||
content: simplemde.value()
|
||||
})
|
||||
simplemde.clearAutosavedValue();
|
||||
let tp = Number("<%= thread.pages %>")
|
||||
let tm = Number("<%= thread.count %>")
|
||||
if (tp * 10 === tm) tp++;
|
||||
if (res) location.href = `/threads/<%= thread.id %>?page=${tp-1}`;
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.fa {
|
||||
color: var(--main);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<% }%>
|
||||
|
||||
<div class="mb-3 clearfix">
|
||||
<nav aria-label="Navigate post pages">
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class="page-item <%= page > 0 ?"": "disabled" %>">
|
||||
<a class="page-link" href="/<%= thread.getLink() %>?page=<%= page-1 %>" tabindex="-1">Back</a>
|
||||
</li>
|
||||
<% for(let i=0; i < thread.pages; i++){ %>
|
||||
<li class="page-item">
|
||||
<a class="page-link <%= i==page?'active':'' %>" href="/<%= thread.getLink() %>?page=<%= i %>"><%= i+1 %>
|
||||
<% if (i==page){ %>
|
||||
<span class="sr-only">(current)</span>
|
||||
<% } %>
|
||||
</a>
|
||||
</li>
|
||||
<% } %>
|
||||
<li class="page-item <%= thread.pages-1 > page ?"": "disabled" %>">
|
||||
<a class="page-link" href="/<%= thread.getLink() %>?page=<%= page+1 %>">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,84 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Thread list!" }) %>
|
||||
|
||||
<body style="text-align: center;">
|
||||
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
<div class="container my-3">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="h4 text-white bg-info mb-0 p-4 rounded-top"><%= title || "Threads" %></h2>
|
||||
<h3 class="h6 text-white bg-info mb-0 p-4 rounded-top"><%= desp %></h2>
|
||||
|
||||
<table class="table table-striped table-bordered table-responsive-lg">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th scope="col" class="topic-col">Topic</th>
|
||||
<th scope="col" class="created-col">Created</th>
|
||||
<th scope="col">Statistics</th>
|
||||
<% if (user?.admin){ %> <th scope="col" class="last-post-col">Action</th> <% } %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<% threads.forEach(thread=>{ %>
|
||||
<tr>
|
||||
<td>
|
||||
<h3 class="h6">
|
||||
<% if (thread.deleted) { %> <span class="badge badge-primary">[DELETED]</span><% } %>
|
||||
<a href="<%= thread.getLink() %>"><%= thread.title %></a>
|
||||
</h3>
|
||||
</td>
|
||||
<td>
|
||||
<div class="avatar">by <a href="/users/<%= thread.authorID %>"><%= thread.author.name %></a><img src="<%=thread.author.avatar %>"></div>
|
||||
<div><%= new Date(thread.time).toLocaleString() %></div>
|
||||
</td>
|
||||
<td>
|
||||
<div><%= thread.messages.length %> messages</div>
|
||||
<div><%= thread.views %> views</div>
|
||||
</td>
|
||||
<% if (user?.admin){ %>
|
||||
<td>
|
||||
<% if (!thread.deleted){ %>
|
||||
<a class="btn-danger" onclick="fetch('/api/threads/<%= thread.id %>/',{method:'DELETE'})"><i class="bx bx-trash bx-sm"></i></a>
|
||||
<% } %>
|
||||
</td>
|
||||
<% } %>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3 clearfix">
|
||||
<nav aria-label="Navigate post pages">
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class="page-item <%= page > 0 ?"": "disabled" %>">
|
||||
<a class="page-link" href="/threads?page=<%= page-1 %>" tabindex="-1">Back</a>
|
||||
</li>
|
||||
<% for(let i=0; i < pages; i++){ %>
|
||||
<li class="page-item">
|
||||
<a class="page-link <%= i==page?'active':'' %>" href="/threads?page=<%= i %>"><%= i+1 %>
|
||||
<% if (i==page){ %>
|
||||
<span class="sr-only">(current)</span>
|
||||
<% } %>
|
||||
</a>
|
||||
</li>
|
||||
<% } %>
|
||||
<li class="page-item <%= pages-1 > page ?"": "disabled" %>">
|
||||
<a class="page-link" href="/threads?page=<%= page+1 %>">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<a href="/threads/create" class="btn btn-lg btn-primary">New Thread</a>
|
||||
</div>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -10,153 +10,156 @@
|
|||
|
||||
|
||||
<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" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js"></script>
|
||||
<style>
|
||||
.img-div {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
<div class="usercontent">
|
||||
.img-div:hover:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
background: url('/themes/bootstrap_black/upload.png') center no-repeat;
|
||||
background-size: 50px;
|
||||
}
|
||||
|
||||
.img-div:hover img {
|
||||
opacity: 0.4;
|
||||
}
|
||||
</style>
|
||||
|
||||
<section class="h-100 gradient-custom-2">
|
||||
|
||||
<div class="container py-5 h-100">
|
||||
<div class="row d-flex justify-content-center align-items-center h-100">
|
||||
<div class="col col-lg-9 col-xl-7">
|
||||
<% if (member.deleted) {%>
|
||||
<h1>This user has been deleted!</h1>
|
||||
<% } %>
|
||||
|
||||
<div class="card">
|
||||
<div class="rounded-top text-white d-flex flex-row" style="background-color: #000; height:200px;">
|
||||
<div class="ms-4 mt-5 d-flex flex-column img-div" style="width: 150px;">
|
||||
<img onclick="location.href='/users/<%=member.id%>/avatar'" src="<%=member.avatar %>" alt="Generic placeholder image" class="img-fluid img-thumbnail mt-4 mb-2" style="width: 150px; z-index: 1">
|
||||
<% if(user?.admin || user?.id === member.id){ %>
|
||||
<a href="/users/<%=member.id%>/edit" type="button" class="btn btn-outline-dark" data-mdb-ripple-color="dark" style="z-index: 1;">
|
||||
Profile Settings
|
||||
</a>
|
||||
<% if (member.deleted) {%>
|
||||
<a id="undelete" class="btn btn-outline-dark">Undelete user</a>
|
||||
<% } else if (user?.admin){ %>
|
||||
<a id="delete" class="btn btn-outline-dark">Delete user</a>
|
||||
<% } %>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
<div class="ms-3 miniinfo" style="margin-top: 75px; background: rgba(0,0,0,0.4);">
|
||||
<h5><%= member.name %></h5>
|
||||
<% if (member.admin) { %>
|
||||
<h2 class="userbox-value" style="align-self: center;">Admin</h2>
|
||||
<% } %>
|
||||
<p style="margin-bottom: 0px;">Join: <%= new Date(member.time).toLocaleDateString() %></p>
|
||||
<% if(!member.hideLastSeen || user?.admin) {%>
|
||||
<p>Last Seen: <%= new Date(member.lastSeen).toLocaleString() %></p>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 text-black" style="background-color: #f8f9fa;">
|
||||
<div class="d-flex justify-content-end text-center py-1">
|
||||
<a href="/search/messages?authorID=<%= member.id %>">
|
||||
<p class="mb-1 h5"><%= counts.message %></p>
|
||||
<p class="small text-muted mb-0">Messages</p>
|
||||
</a>
|
||||
<a class="px-3" href="/search/threads?authorID=<%= member.id %>">
|
||||
<p class="mb-1 h5"><%= counts.thread %></p>
|
||||
<p class="small text-muted mb-0">Threads</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<% if(user?.admin || user?.id === member.id){ %>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<div class="container-fluid">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#userMenu" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="userMenu">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
|
||||
|
||||
<div class="card-body p-4 text-black">
|
||||
<div class="mb-5">
|
||||
<p class="lead fw-normal mb-1" style="color: aliceblue;">About</p>
|
||||
<% if (member.about?.length) { %>
|
||||
<div class="p-4" style="background-color: #f8f9fa;">
|
||||
<p id="about" class="font-italic mb-1"><%= member.about %></p>
|
||||
</div>
|
||||
<script src="/libs/showdown/showdown.min.js"></script>
|
||||
<script>
|
||||
const converter = new showdown.Converter();
|
||||
const about = document.getElementById("about");
|
||||
about.innerHTML = converter.makeHtml(about.innerText);
|
||||
</script>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<!--
|
||||
<p class="lead fw-normal mb-0">Recent photos</p>
|
||||
<p class="mb-0"><a href="#!" class="text-muted">Show all</a></p>
|
||||
-->
|
||||
|
||||
<% if (!member.discordID && discord && user?.id === member.id) { %>
|
||||
<a href="<%=discord%>" class="btn-outline-primary">Discord auth</a>
|
||||
<% } else if(member.discordID && user?.id === member.id) { %>
|
||||
<a class="btn-primary" id="un_discord">Unauth Discord</a>
|
||||
<% } %>
|
||||
<% if (member.hideLastSeen) {%>
|
||||
<a id="last_unhide" class="btn-primary">Unhide last seen</a>
|
||||
<% } else { %>
|
||||
<a id="last_hide" class="btn-outline-primary">Hide last seen</a>
|
||||
<% if (user.admin) { %>
|
||||
<select>
|
||||
<option selected>IP LIST</option>
|
||||
<% for(const ip of member.ips) { %>
|
||||
<option><%= ip %></option>
|
||||
<% } %>
|
||||
</select>
|
||||
<a id="ban" class="btn-outline-primary">Ban All IPs</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>
|
||||
<% } %>
|
||||
<a class="btn-outline-primary" href="/users/<%=member.id%>/edit">Edit user</a>
|
||||
<a href="/users/<%=member.id%>/avatar" class="btn-outline-primary">Upload avatar</a>
|
||||
</ul>
|
||||
|
||||
<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.state !== "DELETED") return
|
||||
alert("User is deleted!");
|
||||
location.reload()
|
||||
} else if (e.target.id == "undelete") {
|
||||
const response = await request("/api/users/<%= member.id %>/", "PATCH", {
|
||||
deleted: false
|
||||
});
|
||||
if (response.state == "DELETED") return;
|
||||
alert("User is undeleted successfully!");
|
||||
location.reload()
|
||||
} else if (e.target.id == "un_discord") {
|
||||
const response = await fetch("/auth/discord/", {
|
||||
method: "DELETE"
|
||||
});
|
||||
alert(await response.text());
|
||||
location.reload()
|
||||
} else if (e.target.id.startsWith("last_")) {
|
||||
let hideLastSeen = e.target.id.replace("last_", "") == "hide" ? true : false;
|
||||
const response = await request("/api/users/<%= member.id %>/", "PATCH", {
|
||||
hideLastSeen
|
||||
});
|
||||
alert(`Last seen is ${!hideLastSeen?"un":""}hided!`);
|
||||
location.reload()
|
||||
|
||||
} else if (e.target.id == "toogle")
|
||||
document.getElementById('user-edit').classList.toggle('no-active')
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</nav>
|
||||
<% if (user.admin) { %>
|
||||
<select>
|
||||
<option selected>IP LIST</option>
|
||||
<% for(const ip of member.ips) { %>
|
||||
<option><%= ip %></option>
|
||||
<% } %>
|
||||
</select>
|
||||
<% } %>
|
||||
|
||||
<% } %>
|
||||
|
||||
|
||||
<div class="userbox" style="justify-content:center;">
|
||||
<img style="width:150px;height:150px;border-radius:50%;" src="<%=member.avatar %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<% if (member.admin) { %>
|
||||
<h2 class="userbox-value" style="align-self: center;">Admin</h2>
|
||||
<% } %>
|
||||
<% if(user?.admin || user?.id === member.id){ %>
|
||||
<script type="module">
|
||||
import request from "../../js/request.js";
|
||||
|
||||
<% if (member.about?.length) { %>
|
||||
<div class="userbox-value" id="about" style="
|
||||
margin: 10px auto;
|
||||
box-shadow: 0 0 5px 0 var(--second);
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
background: none;
|
||||
color: var(--anti);
|
||||
">
|
||||
<%= member.about %>
|
||||
</div>
|
||||
<script>
|
||||
const converter = new showdown.Converter();
|
||||
const about = document.getElementById("about")
|
||||
about.innerHTML = converter.makeHtml(about.innerText);
|
||||
</script>
|
||||
<% } %>
|
||||
document.addEventListener("click", async e => {
|
||||
if (e.target.id == "delete") {
|
||||
const response = await request("/api/users/<%= member.id %>", "DELETE");
|
||||
if (response.state !== "DELETED") return
|
||||
alert("User is deleted!");
|
||||
location.reload()
|
||||
} else if (e.target.id == "undelete") {
|
||||
const response = await request("/api/users/<%= member.id %>/", "PATCH", {
|
||||
deleted: false
|
||||
});
|
||||
if (response.state == "DELETED") return;
|
||||
alert("User is undeleted successfully!");
|
||||
location.reload()
|
||||
} else if (e.target.id == "un_discord") {
|
||||
const response = await fetch("/auth/discord/", {
|
||||
method: "DELETE"
|
||||
});
|
||||
alert(await response.text());
|
||||
location.reload()
|
||||
} else if (e.target.id == "ban") {
|
||||
const response = await fetch("/api/users/<%= member.id %>/ban", { method: "POST" });
|
||||
alert("All IPs of user is banned.");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<% } %>
|
||||
|
||||
|
||||
<div class="userbox">
|
||||
<h2 class="userbox-title">Name:</h2>
|
||||
<h2 class="userbox-value"><%= member.name %></h2>
|
||||
</div>
|
||||
<div class="userbox">
|
||||
<h2 class="userbox-title">Created at:</h2>
|
||||
<h2 class="userbox-value"><%= new Date(member.time).toLocaleString() %></h2>
|
||||
</div>
|
||||
<% if(!member.hideLastSeen || user?.admin) {%>
|
||||
<div class="userbox">
|
||||
<h2 class="userbox-title">Last seen at:</h2>
|
||||
<h2 class="userbox-value"><%= new Date(member.lastSeen).toLocaleString() %></h2>
|
||||
</div>
|
||||
<% } %>
|
||||
<div class="userbox">
|
||||
<h2 class="userbox-title">Message:</h2>
|
||||
<a class="userbox-value" href="/search/messages?authorID=<%= member.id %>">
|
||||
<%= counts.message %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="userbox">
|
||||
<h2 class="userbox-title">Thread:</h2>
|
||||
<a class="userbox-value" href="/search/threads?authorID=<%= member.id %>">
|
||||
<%= counts.thread %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "User list!" }) %>
|
||||
|
||||
|
||||
<body>
|
||||
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<div class="users">
|
||||
<% users.filter(member=> !member.deleted || user.admin ).forEach(member => { %>
|
||||
<div style="display:flex;justify-content:center;">
|
||||
<div class="user-box">
|
||||
<img src="<%= member.avatar %>" class="user-box-img">
|
||||
<div class="user-box-title"> <a href="<%= member.getLink() %>">
|
||||
<% if (member.deleted) { %> <span style="color: var(--important);">[DELETED]</span><% } %>
|
||||
<%= member.name %></a></div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
|
||||
</div>
|
||||
<% if(typeof page === "number"){ %>
|
||||
<div class="mb-3 clearfix">
|
||||
<nav aria-label="Navigate post pages">
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class="page-item <%= page > 0 ?"": "disabled" %>">
|
||||
<a class="page-link" href="/users?page=<%= page-1 %>" tabindex="-1">Back</a>
|
||||
</li>
|
||||
<% for(let i=0; i < pages; i++){ %>
|
||||
<li class="page-item">
|
||||
<a class="page-link <%= i==page?'active':'' %>" href="/users?page=<%= i %>"><%= i+1 %>
|
||||
<% if (i==page){ %>
|
||||
<span class="sr-only">(current)</span>
|
||||
<% } %>
|
||||
</a>
|
||||
</li>
|
||||
<% } %>
|
||||
<li class="page-item <%= pages-1 > page ?"": "disabled" %>">
|
||||
<a class="page-link" href="/users?page=<%= page+1 %>">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<% } %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,115 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Avatar Upload Panel!" }) %>
|
||||
|
||||
<body style="text-align: center;">
|
||||
<link rel="stylesheet" href="/libs/cropper/cropper.css">
|
||||
<style>
|
||||
.container {
|
||||
margin: 20px auto;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.cropper-view-box,
|
||||
.cropper-face {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.cropper-view-box {
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 1px #39f;
|
||||
}
|
||||
</style>
|
||||
<script src="/libs/cropper/cropper.js"></script>
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
|
||||
<div class="container">
|
||||
<h1>Upload avatar for <%= member.name %></h1>
|
||||
|
||||
<input type="file" id="file-input">
|
||||
<img id="image" src="<%= member.avatar %>">
|
||||
<p>
|
||||
<button type="button" id="button">Upload</button>
|
||||
</p>
|
||||
<file id="cropped-image" name="avatar">
|
||||
<form id="form"></form>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
function b64toBlob(b64Data, contentType = "", sliceSize = 512) {
|
||||
|
||||
const byteCharacters = atob(b64Data);
|
||||
const byteArrays = [];
|
||||
|
||||
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
||||
const slice = byteCharacters.slice(offset, offset + sliceSize);
|
||||
|
||||
const byteNumbers = new Array(slice.length);
|
||||
for (let i = 0; i < slice.length; i++)
|
||||
byteNumbers[i] = slice.charCodeAt(i);
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
byteArrays.push(byteArray);
|
||||
}
|
||||
const blob = new Blob(byteArrays, {
|
||||
type: contentType
|
||||
});
|
||||
return blob;
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
const image = document.getElementById('image');
|
||||
const button = document.getElementById('button');
|
||||
const reader = new FileReader();
|
||||
|
||||
document.getElementById("file-input")
|
||||
.addEventListener("change", function() {
|
||||
reader.onload = () => {
|
||||
image.src = reader.result;
|
||||
|
||||
let croppable = false;
|
||||
const cropper = new Cropper(image, {
|
||||
aspectRatio: 1,
|
||||
viewMode: 1,
|
||||
ready: function() {
|
||||
croppable = true;
|
||||
},
|
||||
});
|
||||
|
||||
button.onclick = async () => {
|
||||
if (!croppable) return;
|
||||
|
||||
const croppedCanvas = cropper.getCroppedCanvas();
|
||||
const croppedImage = document.getElementById('cropped-image');
|
||||
const body = new FormData(document.createElement('form'))
|
||||
const block = croppedCanvas.toDataURL().split(";");
|
||||
const contentType = block[0].split(":")[1];
|
||||
const realData = block[1].split(",")[1];
|
||||
body.append('avatar', b64toBlob(realData, contentType));
|
||||
|
||||
const res = await fetch('/api/users/<%= member.id %>/avatar', {
|
||||
method: 'POST',
|
||||
body
|
||||
}).then(res => res.json());
|
||||
|
||||
if (res.error) return alert(res.error);
|
||||
alert('Success!');
|
||||
location.reload();
|
||||
};
|
||||
}
|
||||
reader.readAsDataURL(event.target.files[0]);
|
||||
});
|
||||
</script>
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,44 +0,0 @@
|
|||
<% if (user?.admin){ %>
|
||||
<div class="admin-bar">
|
||||
<a href="/admin" class="admin-bar">Click here to reach admin panel</a>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="header">
|
||||
<a class="logo" href="/"><%= dataset.forum_name.toUpperCase() %> <span>FORUM</span></a>
|
||||
<div class="buttons">
|
||||
|
||||
<% if (user){ %>
|
||||
<a href="<%=user.getLink() %>" class="btn-outline-primary">
|
||||
<div class="box-username"><%= user.name %>
|
||||
<div class="avatar"><img src="<%=user.avatar %>"></div>
|
||||
</div>
|
||||
</a>
|
||||
<a id="logout" href="/login" class="btn-primary">Logout</a>
|
||||
<% } else { %>
|
||||
|
||||
<a id="login" href="/login" class="btn-primary">Login</a>
|
||||
<a href="/register" class="btn-outline-primary">Register</a>
|
||||
<script>
|
||||
document.getElementById("login").href += "?redirect=" + location.pathname;
|
||||
</script>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu">
|
||||
<a href="/threads/create/" class="menu-item">Create Thread</a>
|
||||
<a href="/categories" class="menu-item">Categories</a>
|
||||
<a href="/threads" class="menu-item">Threads</a>
|
||||
<a href="/users" class="menu-item">Users</a>
|
||||
<a href="/search" class="menu-item">Search</a>
|
||||
<script>
|
||||
const menuItems = document.getElementsByClassName("menu-item");
|
||||
|
||||
for (let i = 0; i < menuItems.length; i++)
|
||||
if (window.location.pathname.includes(menuItems[i].getAttribute("href"))) {
|
||||
menuItems[i].classList.add("active-menu");
|
||||
break;
|
||||
}
|
||||
</script>
|
||||
</div>
|
|
@ -1,6 +1,6 @@
|
|||
module.exports = {
|
||||
name: "Default White",
|
||||
codename: "default_white",
|
||||
codename: "default",
|
||||
description: "Default white theme created by Tokmak.",
|
||||
author: "Tokmak"
|
||||
}
|
|
@ -1,5 +1,21 @@
|
|||
/* akf-forum default theme config file */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
||||
|
||||
:root {
|
||||
--main: #4d18e6;
|
||||
--btn-clr-1: #e8e8e8;
|
||||
--menu-item: #ffffff;
|
||||
--borders: #d9d9d9;
|
||||
--input-clr: #414141;
|
||||
--box-shadow: #c3c3c3;
|
||||
--second: #747474;
|
||||
--anti: #151515;
|
||||
--t-username: #555;
|
||||
--important: red;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
@ -640,4 +656,4 @@ USER
|
|||
border-radius: 5px;
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Avatar Upload Panel!" }) %>
|
||||
|
||||
<body style="text-align: center;">
|
||||
<link rel="stylesheet" href="/libs/cropper/cropper.css">
|
||||
<style>
|
||||
.container {
|
||||
margin: 20px auto;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.cropper-view-box,
|
||||
.cropper-face {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.cropper-view-box {
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 1px #39f;
|
||||
}
|
||||
</style>
|
||||
<script src="/libs/cropper/cropper.js"></script>
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
|
||||
<div class="container">
|
||||
<h1>Upload avatar for <%= member.name %></h1>
|
||||
|
||||
<input type="file" id="file-input">
|
||||
<img id="image" src="<%= member.avatar %>">
|
||||
<p>
|
||||
<button type="button" id="button">Upload</button>
|
||||
</p>
|
||||
<file id="cropped-image" name="avatar">
|
||||
<form id="form"></form>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
const MEM_ID="<%= member.id %>";
|
||||
</script>
|
||||
<script src="/js/avatar.js"></script>
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Edit Forum Config!" }) %>
|
||||
|
||||
<body style="text-align: center;">
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<h1>Edit forum config</h1>
|
||||
<textarea rows="30" cols="75"><%= config %></textarea>
|
||||
<a onclick="send();" class="btn-primary">Edit config</a>
|
||||
<script>
|
||||
const textarea = document.querySelector('textarea');
|
||||
async function send() {
|
||||
const res = await fetch('/api/config?text', {
|
||||
method: 'PUT',
|
||||
body: textarea.value,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
if (res.error) return alert(res.error);
|
||||
alert('Success!');
|
||||
textarea.value=JSON.stringify( await res.json(),null,4)
|
||||
|
||||
}
|
||||
</script>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -15,8 +15,8 @@
|
|||
</h1>
|
||||
|
||||
<form id="form" style="box-shadow:none">
|
||||
<input type="text" name="name" placeholder="<%=member.name%>" class="input">
|
||||
|
||||
<input type="text" name="name" placeholder="<%= member.name %>" class="input">
|
||||
<input type="email" name="email" placeholder="<%= member.email %>" class="input">
|
||||
<textarea id="textarea" class="input" name="about" rows="4" cols="60" name="content" placeholder="<%=member.about%>"></textarea>
|
||||
<% if (user?.admin){ %>
|
||||
Is Admin? <input id='admin' type='checkbox' value='true' name='admin' <%=member.admin ? "checked": ""%>>
|
||||
|
@ -40,7 +40,8 @@
|
|||
const res = await request("/api/users/<%=member.id%>", "PATCH", {
|
||||
name: formdata.get("name"),
|
||||
about: simplemde.value(),
|
||||
admin: formdata.get("admin")
|
||||
admin: formdata.get("admin"),
|
||||
email: formdata.get("email")
|
||||
});
|
||||
simplemde.clearAutosavedValue();
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: type+" error!" }) %>
|
||||
|
||||
|
||||
<body style="text-align: center;">
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
<h1 style="color: var(--main);"><%= type %></h1>
|
||||
<h2 style="color: var(--second);"><%= error %></h2>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -4,7 +4,8 @@
|
|||
<% for(const theme of dataset.themes){%>
|
||||
<option value="<%= theme.codename %>"><%= theme.name %></option>
|
||||
<% } %>
|
||||
</select>
|
||||
</select> <script src="/js/theme.js"></script>
|
||||
|
||||
<script>
|
||||
const theme_select = document.getElementById("theme_select");
|
||||
theme_select.querySelector(`option[value=<%= user.theme.codename %>]`).selected = true;
|
|
@ -1,9 +1,8 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title><%= title || dataset.forum_name +"-forum" %></title>
|
||||
<title><%= title || dataset.forum_name %></title>
|
||||
<meta name="description" content="<%= dataset.description %>">
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
<link rel="stylesheet" href="/themes/default_black/main.css" />
|
||||
<link rel="stylesheet" href="/themes/common/main.css" />
|
||||
<link rel="stylesheet" href="/themes/default/main.css" />
|
||||
</head>
|
|
@ -0,0 +1,44 @@
|
|||
<% if (user?.admin){ %>
|
||||
<div class="admin-bar">
|
||||
<a href="/admin" class="admin-bar">Click here to reach admin panel</a>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="header">
|
||||
<a class="logo" href="/"><%= dataset.forum_name.toUpperCase() %></a>
|
||||
<div class="buttons">
|
||||
|
||||
<% if (user){ %>
|
||||
<a href="<%=user.getLink() %>" class="btn-outline-primary">
|
||||
<div class="box-username"><%= user.name %>
|
||||
<div class="avatar"><img src="<%=user.avatar %>"></div>
|
||||
</div>
|
||||
</a>
|
||||
<a id="logout" href="/login" class="btn-primary">Logout</a>
|
||||
<% } else { %>
|
||||
|
||||
<a id="login" href="/login" class="btn-primary">Login</a>
|
||||
<a href="/register" class="btn-outline-primary">Register</a>
|
||||
<script>
|
||||
document.getElementById("login").href += "?redirect=" + location.pathname;
|
||||
</script>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu">
|
||||
<a href="/threads/create/" class="menu-item">Create Thread</a>
|
||||
<a href="/categories" class="menu-item">Categories</a>
|
||||
<a href="/threads" class="menu-item">Threads</a>
|
||||
<a href="/users" class="menu-item">Users</a>
|
||||
<a href="/search" class="menu-item">Search</a>
|
||||
<script>
|
||||
const menuItems = document.getElementsByClassName("menu-item");
|
||||
|
||||
for (let i = 0; i < menuItems.length; i++)
|
||||
if (window.location.pathname.includes(menuItems[i].getAttribute("href"))) {
|
||||
menuItems[i].classList.add("active-menu");
|
||||
break;
|
||||
}
|
||||
</script>
|
||||
</div>
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Welcome to the "+dataset.forum_name+"-forum!" }) %>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Welcome to the "+dataset.forum_name }) %>
|
||||
|
||||
|
||||
<body>
|
|
@ -0,0 +1,35 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title:"Message search!" }) %>
|
||||
|
||||
|
||||
<body style="text-align: center;">
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
<div id="messages">
|
||||
|
||||
<% messages.filter(Boolean).forEach(message=>{ %>
|
||||
|
||||
<div class="message" id="message-<%= message.id %>">
|
||||
<div class="left">
|
||||
<img src="<%= message.author.avatar %>" />
|
||||
<div class="username"><a href="/users/<%=message.authorID %>"><%=message.author.name %></a></div>
|
||||
<div class="date">
|
||||
<%= new Date(message.time).toLocaleDateString() %>
|
||||
</div>
|
||||
<div class="date">
|
||||
<%= new Date(message.time).toLocaleTimeString() %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content"><%- message.content %></div>
|
||||
</div>
|
||||
|
||||
<% }); %>
|
||||
</div>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Register!" }) %>
|
||||
|
||||
|
||||
<body style="text-align: center;">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
<h1 class="title">Register</h1>
|
||||
|
||||
|
||||
<form class="login" action="/register" method="post">
|
||||
<input type="email" name="email" placeholder="Email" class="input" required>
|
||||
<input type="text" name="name" placeholder="Username" class="input" required>
|
||||
<input type="password" name="password" placeholder="Password" class="input" required>
|
||||
<textarea class="input" name="about" rows="4" placeholder="About you... You can use markdown"></textarea>
|
||||
<input type="submit" class="btn-primary" style="width:100%;" value="Register">
|
||||
</form>
|
||||
<% if(discord) { %>
|
||||
<a href="<%=discord%>" class="btn-outline-primary">Auth with discord</a>
|
||||
<% } %>
|
||||
<a href="/login" class="btn-outline-primary">Login</a>
|
||||
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,35 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Search page!" }) %>
|
||||
|
||||
|
||||
<body style="text-align: center;">
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
<h1 class="title">Search</h1>
|
||||
|
||||
<div class="search">
|
||||
<form action="/search/users" method="GET">
|
||||
<input type="text" class="input" name="q" placeholder="Search for users" />
|
||||
<input type="submit" class="btn-primary" value="Search" />
|
||||
</form>
|
||||
|
||||
<form action="/search/messages" method="GET">
|
||||
<input type="text" class="input" name="q" placeholder="Search for messages" />
|
||||
<input type="text" class="input" name="authorID" placeholder="(Not Required) Author id" />
|
||||
<input type="submit" value="Search" class="btn-primary" />
|
||||
</form>
|
||||
|
||||
<form action="/search/threads" method="GET">
|
||||
<input type="text" class="input" name="q" placeholder="Search for threads" />
|
||||
<input type="text" class="input" name="authorID" placeholder="(Not Required) Author id" />
|
||||
<input type="submit" value="Search" class="btn-primary" />
|
||||
</form>
|
||||
</div>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,23 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Log in!" }) %>
|
||||
|
||||
<body style="text-align: center;">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
<h1 class="title">Change password</h1>
|
||||
|
||||
<form class="login" action="/security" method="post">
|
||||
<input type="password" name="old_password" placeholder="Old Password" class="input" required>
|
||||
<input type="password" name="password" placeholder="Password" class="input" required>
|
||||
<input type="submit" style="width:100%" class="btn-primary" value="Change it">
|
||||
</form>
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Setup the Akf-forum!" }) %>
|
||||
|
||||
|
||||
<body style="text-align: center;">
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
|
||||
<h1 style="color: var(--main);">Setup</h1>
|
||||
<h2 style="color: var(--second);">There is default settings for akf-forum, you not need to edit them, but you can! And, the first registered user will be admin.</h2>
|
||||
<form class="search" method="POST">
|
||||
Forum name:
|
||||
<input class="input" type="text" name="forum_name" value="akf-forum" required>
|
||||
Forum description:
|
||||
<input class="input" type="text" name="description" value="Akf-forum!" required>
|
||||
Default state for new threads, change with "APPROVAL" for approval system:
|
||||
<input class="input" type="text" name="default_thread_state" value="OPEN" required>
|
||||
Default state for new users, change with "APPROVAL" for approval system:
|
||||
<input class="input" type="text" name="default_user_state" value="ACTIVE" required>
|
||||
Domain of the forum, defaulty setted:
|
||||
<input class="input" type="text" name="host" id="domain" value="Akf-forum!" required>
|
||||
|
||||
<input type="submit" class="btn-primary" value="Setup">
|
||||
</form>
|
||||
<script>
|
||||
document.getElementById("domain").value = location.origin;
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -7,7 +7,7 @@
|
|||
<body>
|
||||
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js"></script>
|
||||
<script src="/libs/showdown/showdown.min.js"></script>
|
||||
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
|
||||
<link rel="stylesheet" href="/libs/simplemde/simplemde.min.css">
|
||||
<script src="/libs/simplemde/simplemde.min.js"></script>
|
|
@ -10,7 +10,6 @@
|
|||
|
||||
|
||||
<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" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js"></script>
|
||||
|
||||
<div class="usercontent">
|
||||
|
||||
|
@ -24,7 +23,7 @@
|
|||
<a class="btn-outline-primary" id="un_discord">Unauth DC!</a>
|
||||
<% } %>
|
||||
<a href="/users/<%=member.id%>/avatar" class="btn-outline-primary">Upload avatar</a>
|
||||
<a class="btn-outline-primary" id="toogle">Edit user!</a>
|
||||
<a class="btn-outline-primary" href="/users/<%=member.id%>/edit" id="toogle">Edit user!</a>
|
||||
<script type="module">
|
||||
import request from "/js/request.js";
|
||||
|
||||
|
@ -57,9 +56,7 @@
|
|||
alert(`Last seen is ${!hideLastSeen?"un":""}hided!`);
|
||||
location.reload()
|
||||
|
||||
} else if (e.target.id == "toogle")
|
||||
document.getElementById('user-edit').classList.toggle('no-active')
|
||||
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -110,6 +107,7 @@ color: var(--anti);
|
|||
">
|
||||
<%= member.about %>
|
||||
</div>
|
||||
<script src="/libs/showdown/showdown.min.js"></script>
|
||||
<script>
|
||||
const converter = new showdown.Converter();
|
||||
const about = document.getElementById("about")
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
name: "Default Black",
|
||||
codename: "default_black",
|
||||
description: "A black theme over the default white theme. Edited by Akif9748, Created by Tokmak.",
|
||||
author: "Akif9748 & Tokmak"
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
akf-forum black theme config file
|
||||
*/
|
||||
:root {
|
||||
--main: #ac8fff;
|
||||
--btn-clr-1: #e8e8e8;
|
||||
--menu-item: #ffffff;
|
||||
--borders: #d9d9d9;
|
||||
--input-clr: #dcdcdc;
|
||||
--box-shadow: #c3c3c3;
|
||||
--second: #9f9f9f;
|
||||
--anti: #ebebeb;
|
||||
--t-username: rgb(236 236 236);
|
||||
background-color: #000000;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
<%- include(dataset.getFile("common/views/extra/footer")) %>
|
|
@ -1 +0,0 @@
|
|||
<%- include(dataset.getFile("common/views/extra/navbar")) %>
|
|
@ -1 +0,0 @@
|
|||
<%- include(dataset.getFile("common/views/extra/usermenu")) %>
|
|
@ -1,15 +0,0 @@
|
|||
/* akf-forum default theme config file */
|
||||
|
||||
:root {
|
||||
--main: #4d18e6;
|
||||
--btn-clr-1: #e8e8e8;
|
||||
--menu-item: #ffffff;
|
||||
--borders: #d9d9d9;
|
||||
--input-clr: #414141;
|
||||
--box-shadow: #c3c3c3;
|
||||
--second: #747474;
|
||||
--anti: #151515;
|
||||
--t-username: #555;
|
||||
--important: red;
|
||||
background-color: #ffffff;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
<%- include(dataset.getFile("common/views/extra/footer")) %>
|
|
@ -1,9 +0,0 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title><%= title || dataset.forum_name +"-forum" %></title>
|
||||
<meta name="description" content="<%= dataset.description %>">
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
<link rel="stylesheet" href="/themes/default_white/main.css" />
|
||||
<link rel="stylesheet" href="/themes/common/main.css" />
|
||||
</head>
|
|
@ -1 +0,0 @@
|
|||
<%- include(dataset.getFile("common/views/extra/navbar")) %>
|
|
@ -1 +0,0 @@
|
|||
<%- include(dataset.getFile("common/views/extra/usermenu")) %>
|
|
@ -1,7 +1,7 @@
|
|||
const mongoose = require("mongoose");
|
||||
require("dotenv").config();
|
||||
|
||||
mongoose.connect(process.env.MONGO_DB_URL, () => console.log("Database is connected"));
|
||||
mongoose.connect(process.env.MONGO_DB_URL);
|
||||
|
||||
const { UserModel } = require("../src/models");
|
||||
(async () => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const mongoose = require("mongoose");
|
||||
require("dotenv").config();
|
||||
|
||||
mongoose.connect(process.env.MONGO_DB_URL, () => console.log("Database is connected"));
|
||||
mongoose.connect(process.env.MONGO_DB_URL);
|
||||
|
||||
const Models = require("../src/models");
|
||||
|
||||
|
|
Loading…
Reference in New Issue