Compare commits

...

4 Commits
v5.6.0 ... main

Author SHA1 Message Date
Akif9748 8c20c70874 5.7.0 2024-02-10 23:51:48 +03:00
Akif9748 9b5bb9b782 Themes folder update
- discord better auth system with email
- module updates
- Cache is working now
- avatar.js extruted
- pagination++
- ban and password change button on interface
2024-02-10 23:51:42 +03:00
Akif9748 0438e4fdb4 5.6.1 2023-08-27 20:22:49 +03:00
Akif9748 00ea899fb4 Clear online guest, new dc username 2023-08-27 20:22:28 +03:00
82 changed files with 2496 additions and 36892 deletions

View File

@ -44,8 +44,6 @@ Akf-forum has got an API for AJAX (fetch), other clients etc. And, you can learn
![image](https://github.com/Akif9748/akf-forum/assets/70021050/1ad4ad8e-d000-46a6-834e-7d76cdddda60)
## TO-DO list
#### Add used modules etc like: cropper:
https://github.com/akashgiricse/Online-Forum
### Backend:
#### Feature:
@ -62,34 +60,41 @@ permissions: ["see_deleted_message"]
#### Fixes:
- Admin deleting other admins.
- send public to common/public
- 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
- add used open source libraries to README.md
### Frontend
#### Features:
- change category name
- Add approval threads page.
- add support for trans around gravatar and upload photo
- add support for switch around gravatar and upload photo
- old contents / titles add to forum interface
- who liked a message
- add ban button to user profile
#### Fixes:
- BETTER SETUP PAGE: use setup everytime
- add threads, messages etc. to extra folder
- add threads, messages etc. to "extra" folder
- add category to thread list page
- working reset button
- better pagination
- 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
## We used:
## Special Thanks:
https://github.com/akashgiricse/Online-Forum for bootstrap theme.
@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

View File

@ -13,7 +13,7 @@
},
"global_ratelimit": {
"enabled": true,
"max": 25,
"max": 30,
"windowMs": 60000
},
"discord_auth": false,

4680
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,12 @@
{
"name": "akf-forum",
"version": "5.6.0",
"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"
}
}

View File

@ -20,31 +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);
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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

63
src/public/js/avatar.js Normal file
View File

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

View File

@ -1,28 +1,29 @@
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, users, threads, messages, newestMember, newestMessages, newestThreads, onlineUserCount, onlineUsers
categories, users, threads, messages, newestMember, newestMessages, newestThreads, onlineMemberCount, onlineMembers
] = await Promise.all([
CategoryModel.count(),
UserModel.count({ deleted: false }),
ThreadModel.count({ state: "OPEN" }),
MessageModel.count({ deleted: false }),
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.count({ deleted: false, lastSeen: { $gt: Date.now() - 1000 * 60 * 5 } }),
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, newestMessages, newestThreads,
onlineUserCount, onlineUsers
onlineMemberCount, onlineMembers, onlineGuests, onlineTotal
});
});

View File

@ -5,7 +5,7 @@ const { themes, emailRegEx } = require("../../../lib")
const app = Router();
const { join } = require("path");
app.param("id", async (req, res, next, id) => {
if (id === "me")
if (id === "me") //
id = req.user.id;
req.member = await UserModel.get(id, req.user.admin ? "+lastSeen +ips" : "");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -47,21 +47,23 @@
<% if(typeof page === "number"){ %>
<div class="mb-3 clearfix">
<nav aria-label="Navigate post pages" class="float-lg-right">
<ul class="pagination pagination-sm mb-lg-0">
<% if (page > 0){ %>
<li class="page-item"><a href="/categories?page=<%= page-1 %>" class="page-link">Back</a></li>
<% } %>
<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 <%= i==page?'active':'' %>"><a href="/categories?page=<%= i %>" class="page-link"><%= i+1 %>
<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>
<% } %>
<% if (pages-1 > page) { %>
<li class="page-item"><a href="/categories?page=<%= page+1 %>" class="page-link">Next</a></li>
</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>

View File

@ -12,6 +12,8 @@
<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">

View File

@ -36,30 +36,30 @@
</thead>
<tbody>
<% newestThreads.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>
<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>
<% } %>
</tr>
<% }); %>
</td>
<% } %>
</tr>
<% }); %>
</tbody>
</table>
</div>
@ -103,18 +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>
<% onlineUsers.forEach(user => { %>
<a href="/users/<%=user.id %>"><%= user.name %></a>
<% }); %>
<% 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"><%= onlineUserCount %></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"><%= onlineMemberCount %></dd>
</dl>
<dl class="row mb-0">
<dt class="col-8">Guests:</dt>
<dd class="col-4 mb-0"><%= onlineGuests %></dd>
</dl>
</div>
</div>
@ -139,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="/users/<% newestMember.id %>"><%= newestMember.name %></a></div>
<div><a href="/users/<%= newestMember.id %>"><%= newestMember.name %></a></div>
</div>
<% }%>
</div>
</div>
</div>

View File

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

View File

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

View File

@ -142,21 +142,23 @@
<% }%>
<div class="mb-3 clearfix">
<nav aria-label="Navigate post pages" class="float-lg-right">
<ul class="pagination pagination-sm mb-lg-0">
<% if (page > 0){ %>
<li class="page-item"><a href="/<%= thread.getLink() %>?page=<%= page-1 %>" class="page-link">Back</a></li>
<% } %>
<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 <%= i==page?'active':'' %>"><a href="/<%= thread.getLink() %>?page=<%= i %>" class="page-link"><%= i+1 %>
<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>
<% } %>
<% if (thread.pages-1 > page) { %>
<li class="page-item"><a href="/<%= thread.getLink() %>?page=<%= page+1 %>" class="page-link">Next</a></li>
</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>

View File

@ -55,21 +55,23 @@
</div>
</div>
<div class="mb-3 clearfix">
<nav aria-label="Navigate post pages" class="float-lg-right">
<ul class="pagination pagination-sm mb-lg-0">
<% if (page > 0){ %>
<li class="page-item"><a href="/threads?page=<%= page-1 %>" class="page-link">Back</a></li>
<% } %>
<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 <%= i==page?'active':'' %>"><a href="/threads?page=<%= i %>" class="page-link"><%= i+1 %>
<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>
<% } %>
<% if (pages-1 > page) { %>
<li class="page-item"><a href="/threads?page=<%= page+1 %>" class="page-link">Next</a></li>
</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>

View File

@ -47,7 +47,7 @@
<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;">
Edit profile
Profile Settings
</a>
<% if (member.deleted) {%>
<a id="undelete" class="btn btn-outline-dark">Undelete user</a>
@ -83,28 +83,29 @@
<% if (member.about?.length) { %>
<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")
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) { %>
@ -117,9 +118,8 @@
<option><%= ip %></option>
<% } %>
</select>
<a id="ban" class="btn-outline-primary">Ban All IPs</a>
<% } %>
</div>
</div>
</div>
@ -128,53 +128,38 @@
</div>
</section>
<% if(user?.admin || user?.id === member.id){ %>
<script type="module">
import request from "../../js/request.js";
<div class="usercontent">
<% if(user?.admin || user?.id === member.id){ %>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
</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 == "toogle")
document.getElementById('user-edit').classList.toggle('no-active')
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
});
</script>
</nav>
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>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>

View File

@ -24,21 +24,23 @@
</div>
<% if(typeof page === "number"){ %>
<div class="mb-3 clearfix">
<nav aria-label="Navigate post pages" class="float-lg-right">
<ul class="pagination pagination-sm mb-lg-0">
<% if (page > 0){ %>
<li class="page-item"><a href="/users?page=<%= page-1 %>" class="page-link">Back</a></li>
<% } %>
<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 <%= i==page?'active':'' %>"><a href="/users?page=<%= i %>" class="page-link"><%= i+1 %>
<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>
<% } %>
<% if (pages-1 > page) { %>
<li class="page-item"><a href="/users?page=<%= page+1 %>" class="page-link">Next</a></li>
</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>

View File

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

View File

@ -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() %></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>

View File

@ -1,6 +1,6 @@
module.exports = {
name: "Default White",
codename: "default_white",
codename: "default",
description: "Default white theme created by Tokmak.",
author: "Tokmak"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,5 @@
<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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@
<%- include(dataset.getFile("common/views/extra/footer")) %>

View File

@ -1 +0,0 @@
<%- include(dataset.getFile("common/views/extra/navbar")) %>

View File

@ -1 +0,0 @@
<%- include(dataset.getFile("common/views/extra/usermenu")) %>

View File

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

View File

@ -1 +0,0 @@
<%- include(dataset.getFile("common/views/extra/footer")) %>

View File

@ -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 %></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>

View File

@ -1 +0,0 @@
<%- include(dataset.getFile("common/views/extra/navbar")) %>

View File

@ -1 +0,0 @@
<%- include(dataset.getFile("common/views/extra/usermenu")) %>

View File

@ -1,6 +0,0 @@
module.exports = {
name: "Xenforo Black Theme",
codename: "xenforo",
description: "Xenforo theme by xenforo, and edited by Akif9748",
author: "Akif9748"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,47 +0,0 @@
<!DOCTYPE html>
<html id="XF" lang="tr-TR" dir="LTR" data-app="public" data-template="forum_list" data-container-key data-content-key data-logged-in="true" data-cookie-prefix="xf_" data-csrf="1685091272,f052c88e4c4aa12fbe73a08e269625fa" class="has-no-js v_2_0 template-forum_list">
<%- include("extra/meta", {title: "Error" }) %>
<body data-template="forum_list">
<div class="p-pageWrapper" id="top">
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
<div class="p-body">
<div class="p-body-inner">
<div class="p-body-header">
<div class="p-title ">
<h1 class="p-title-value">Hay aksi! Bir sorun yaşıyoruz.</h1>
</div>
</div>
<div class="p-body-main ">
<div class="p-body-contentCol"></div>
<div class="p-body-content">
<div class="p-body-pageContent">
<div class="blockMessage">
İstenen sayfa bulunamadı.
</div>
</div>
</div>
</div>
<ul class="p-breadcrumbs p-breadcrumbs--bottom" itemscope="" itemtype="https://schema.org/BreadcrumbList">
<li itemprop="itemListElement" itemscope="" itemtype="https://schema.org/ListItem">
<a href="https://makale.silicone-forum.com" itemprop="item">
<span itemprop="name">Ana sayfa</span>
</a>
<meta itemprop="position" content="1">
</li>
</ul>
</div>
</div>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</div>
<div class="u-scrollButtons js-scrollButtons" data-trigger-type="up">
<a href="#top" class="button--scroll button" data-xf-click="scroll-to"><span class="button-text"><i class="fa--xf far fa-arrow-up" aria-hidden="true"></i><span class="u-srOnly">Top</span></span></a>
</div>
</body>
</html>

View File

@ -1,64 +0,0 @@
<footer class="p-footer" id="footer">
<div class="p-footer-inner">
<div class="p-footer-row">
<div class="p-footer-row-main">
<ul class="p-footer-linkList">
<li>
<% if (user){ %>
<select id="theme_select">
<% for(const theme of dataset.themes){%>
<option value="<%= theme.codename %>"><%= theme.name %></option>
<% } %>
</select>
<script>
const theme_select = document.getElementById("theme_select");
theme_select.querySelector(`option[value=<%= user.theme.codename %>]`).selected = true;
theme_select.addEventListener("change", async e => {
const codename = e.target.value;
await fetch('/api/users/<%= user.id %>', {
method: 'PATCH',
body: JSON.stringify({
theme: {
codename
}
}),
headers: {
"Content-Type": "application/json"
}
});
const theme = await fetch("/api/themes/" + codename).then(res => res.json());
const txt = "Theme changed to:\n" +
"Name: " + theme.name + "\n" +
"Description: " + theme.description + "\n" +
"Author: " + theme.author + "\n";
alert(txt)
location.reload();
});
</script>
<% } %>
</li>
</ul>
</div>
<div class="p-footer-row-opposite">
<ul class="p-footer-linkList">
<li><a href="/">Ana sayfa</a></li>
</ul>
</div>
</div>
<div class="p-footer-copyright">
Xenforo theme created <a href="https://xenforo.com" class="u-concealed" dir="ltr" target="_blank" rel="sponsored noopener">by XenForo<sup>&reg;</sup> </a>
and <a href="https://github.com/Akif9748"> edited by Akif9748</a>
<br>This website is powered by <a class="text-white" href="https://github.com/Akif9748/akf-forum">akf-forum</a>
</div>
</div>
</footer>
<footer class="text-center text-white fixed-bottom">
<div class="text-center p-3">
</div>
</footer>

View File

@ -1,20 +0,0 @@
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title><%= title || dataset.forum_name %></title>
<meta name="theme-color" content="#1e1e1e" />
<meta name="apple-mobile-web-app-title" content="Silicone">
<link rel="alternate" type="application/rss+xml" title="RSS feed for <%= title || dataset.forum_name %>" href="/bolum/-/index.rss" />
<meta property="og:site_name" content="<%= title || dataset.forum_name %>" />
<meta property="og:type" content="website" />
<meta property="og:title" content="<%= title || dataset.forum_name %>" />
<meta property="twitter:title" content="<%= title || dataset.forum_name %>" />
<meta name="description" content="<%= dataset.description %>">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<meta property="og:description" content="<%= dataset.description %>" />
<meta property="twitter:description" content="<%= dataset.description %>" />
<link rel="stylesheet" href="/themes/xenforo/s1.css" />
<link rel="stylesheet" href="/themes/xenforo/s2.css" />
</head>

View File

@ -1,95 +0,0 @@
<header class="p-header" id="header">
<div class="p-header-inner">
<div class="p-header-content">
<a style="font-size: 40px;" href="/"><%= dataset.forum_name.toUpperCase() %></a>
</div>
</div>
</header>
<div class="p-navSticky p-navSticky--primary" data-xf-init="sticky-header">
<nav class="p-nav">
<div class="p-nav-inner">
<button type="button" class="button--plain p-nav-menuTrigger button" data-xf-click="off-canvas" data-menu=".js-headerOffCanvasMenu" tabindex="0" aria-label="Menü"><span class="button-text">
<i aria-hidden="true"></i>
</span></button>
<div class="p-nav-scroller hScroller" data-xf-init="h-scroller" data-auto-scroll=".p-navEl.is-selected">
<div class="hScroller-scroll">
<ul class="p-nav-list js-offCanvasNavSource">
<li>
<div class="p-navEl">
<a class="p-navEl-link p-navEl-link--splitMenu " href="/threads/create/">Create Thread</a>
</div>
</li>
<li>
<div class="p-navEl">
<a class="p-navEl-link p-navEl-link--splitMenu " href="/categories">Categories</a>
</div>
</li>
<li>
<div class="p-navEl">
<a class="p-navEl-link p-navEl-link--splitMenu " href="/threads">Threads</a>
</div>
</li>
<li>
<div class="p-navEl">
<a class="p-navEl-link p-navEl-link--splitMenu " href="/users">Users</a>
</div>
</li>
<li>
<div class="p-navEl">
<a class="p-navEl-link p-navEl-link--splitMenu " href="/search">Search</a>
</div>
</li>
<li>
<div class="p-navEl">
<a class="p-navEl-link p-navEl-link--splitMenu " href="/login">Logout</a>
</div>
</li>
<script>
const menuItems = document.getElementsByClassName("p-navEl");
for (let i = 0; i < menuItems.length; i++)
if (window.location.pathname.includes(menuItems[i].children[0].getAttribute("href"))) {
menuItems[i].classList.add("is-selected");
break;
}
</script>
</ul>
</div>
</div>
<div class="p-nav-opposite">
<div class="p-navgroup p-account p-navgroup--member">
<a href="<%=user.getLink()%>" class="p-navgroup-link p-navgroup-link--iconic p-navgroup-link--user" data-xf-click="menu" data-xf-key="m" data-menu-pos-ref="< .p-navgroup" title="Akif" aria-expanded="false" aria-haspopup="true">
<span class="avatar avatar--xxs" data-user-id="116">
<img src="<%=user.avatar %>" alt="Akif" class="avatar-u116-s" width="48" height="48" loading="lazy" />
</span>
<span class="p-navgroup-linkText"><%=user.name %></span>
</a>
</div>
<div class="p-navgroup p-discovery">
<a href="/search" class="p-navgroup-link p-navgroup-link--iconic p-navgroup-link--search" data-xf-click="menu" data-xf-key="/" aria-label="Ara" aria-expanded="false" aria-haspopup="true" title="Ara">
<i aria-hidden="true"></i>
<span class="p-navgroup-linkText">Ara</span>
</a>
</div>
</div>
</div>
</nav>
</div>
<% if (user?.admin){ %>
<div class="p-sectionLinks">
<div class="p-sectionLinks-inner hScroller" data-xf-init="h-scroller">
<div class="hScroller-scroll">
<ul class="p-sectionLinks-list">
<li>
<div class="p-navEl ">
<a href="/admin" class="p-navEl-link ">Admin Page</a>
</div>
</li>
</ul>
</div>
</div>
</div>
<% } %>

View File

@ -1,146 +0,0 @@
<!DOCTYPE html>
<html id="XF">
<%- include("extra/meta", {title: "Welcome to the "+dataset.forum_name+"-forum!" }) %>
<body data-template="forum_list">
<div class="p-pageWrapper" id="top">
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
<div class="p-body">
<div class="p-body-inner">
<div class="p-body-header">
<div class="p-title ">
<h1 class="p-title-value"><%=dataset.forum_name%></h1>
<div class="p-title-pageAction">
<a href="/threads/create" class="button--cta button button--icon button--icon--write" data-xf-click="overlay" rel="nofollow"><span class="button-text">
Create Thread
</span></a>
</div>
</div>
</div>
<div class="p-body-main p-body-main--withSidebar ">
<div class="p-body-contentCol"></div>
<div class="p-body-sidebarCol"></div>
<div class="p-body-content">
<div class="p-body-pageContent">
<div class="block" data-widget-id="24" data-widget-key="new_threads_ust" data-widget-definition="new_threads">
<div class="block-container">
<h3 class="block-header">
<a href="/threads" rel="nofollow">New Threads</a>
</h3>
<div class="block-body">
<div class="structItemContainer">
<% newestThreads.forEach(thread => { %>
<div class="structItem structItem--thread js-inlineModContainer js-threadListItem-14359" data-author="ercncavs">
<div class="structItem-cell structItem-cell--icon">
<div class="structItem-iconContainer">
<a href="" class="avatar avatar--s" data-xf-init="member-tooltip">
<img src="<%= thread.author.avatar %>" class="avatar-u1804-s" width="48" height="48" loading="lazy" />
</a>
</div>
</div>
<div class="structItem-cell structItem-cell--main" data-xf-init="touch-proxy">
<div class="structItem-title">
<a href="<%= thread.getLink() %>" data-tp-primary="on" data-xf-init="preview-tooltip"><%= thread.title %></a>
</div>
<div class="structItem-minor">
<ul class="structItem-parts">
<li>
<a href="/users/<%= thread.authorID %>" class="username " dir="auto" data-user-id="1804" data-xf-init="member-tooltip">
<%= thread.author.name %>
</a>
</li>
<li class="structItem-startDate">
<a>
<time class="u-dt"><%= new Date(thread.time).toLocaleString() %></time>
</a>
</li>
<li><a href="/categories/<%= thread.categoryID %>"><%= thread.categoryID %></a></li>
</ul>
</div>
</div>
<div class="structItem-cell structItem-cell--meta" title="İlk mesajın ifade puanı: 0">
<dl class="pairs pairs--justified">
<dt>Messages</dt>
<dd><%= thread.messages.length %></dd>
</dl>
<dl class="pairs pairs--justified structItem-minor">
<dt>Views</dt>
<dd><%= thread.views %></dd>
</dl>
</div>
</div>
<% }) %>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="p-body-sidebar">
<div class="block" data-widget-section="onlineNow" data-widget-id="6" data-widget-key="forum_overview_members_online" data-widget-definition="members_online">
<div class="block-container">
<h3 class="block-minorHeader"><a href="/">Online Members</a></h3>
<div class="block-body">
<div class="block-row block-row--minor">
<ul class="listInline listInline--comma">
<% onlineUsers.forEach(user => { %>
<li><a href="/users/<%=user.id %>" class="username " dir="auto" data-user-id="116" data-xf-init="member-tooltip"><%= user.name %></a></li>
<% }); %>
</ul>
</div>
</div>
<div class="block-footer">
<span class="block-footer-counter">Total: <%= onlineUserCount %>
</span>
</div>
</div>
</div>
<div class="block" data-widget-id="9" data-widget-key="forum_overview_forum_statistics" data-widget-definition="forum_statistics">
<div class="block-container">
<h3 class="block-minorHeader">Forum statistics</h3>
<div class="block-body block-row">
<dl class="pairs pairs--justified">
<dt>Total categories</dt>
<dd><%= categories %></dd>
</dl>
<dl class="pairs pairs--justified">
<dt>Total threads</dt>
<dd><%= threads %></dd>
</dl>
<dl class="pairs pairs--justified">
<dt>Total messages</dt>
<dd><%= messages %></dd>
</dl>
<dl class="pairs pairs--justified">
<dt>Total members</dt>
<dd><%= users %></dd>
</dl>
<dl class="pairs pairs--justified">
<dt>Newest member</dt>
<dd><a href="/users/<% newestMember.id %>" class="username " dir="auto" data-xf-init="member-tooltip"><%= newestMember.name %></a></dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -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 () => {

View File

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