Compare commits

...

14 Commits

Author SHA1 Message Date
Akif9748 922b8e5732 5.2.6 2023-05-25 19:28:15 +03:00
Akif9748 9ee1d589eb Better pagination and rewiretten threads etc. 2023-05-25 19:28:05 +03:00
Akif9748 83ae8507c0
Update README.md 2023-05-25 18:07:18 +03:00
Akif9748 e3b6345196 fix 2023-05-25 18:03:22 +03:00
Akif9748 17486e4288 5.2.5 2023-05-25 18:00:58 +03:00
Akif9748 8e1eff30f5 usermenu not needs to templates 2023-05-25 18:00:53 +03:00
Akif9748 a77bee5fde 5.2.4 2023-05-25 17:58:53 +03:00
Akif9748 31b0e86d09 better discord auth 2023-05-25 17:58:47 +03:00
Akif9748 dced1b0a02 5.2.3 2023-05-25 17:48:07 +03:00
Akif9748 2a8e26b7b2 better folder system 2023-05-25 17:47:54 +03:00
Akif9748 78e78c0694 5.2.2 2023-05-25 17:23:41 +03:00
Akif9748 edf9fc83dc full support for multiple themes, secure templates 2023-05-25 17:23:31 +03:00
Akif9748 080e1e6ced 5.2.1 2023-05-25 16:15:57 +03:00
Akif9748 adb19e2e34 setup and index is now differnt 2023-05-25 16:15:50 +03:00
115 changed files with 1362 additions and 551 deletions

View File

@ -1,6 +1,7 @@
MONGO_DB_URL = mongodb://localhost:27017/akf-forum
SECRET = secret
DISCORD_SECRET = yourDiscordSecret
DISCORD_ID = yourDiscordId
EMAIL_USER =
EMAIL_PASS =
EMAIL_SERVICE =

View File

@ -20,7 +20,7 @@
"browser": true
},
"files": [
"public/**"
"src/public/js/*"
]
}
]

View File

@ -6,18 +6,19 @@ A Node.js based forum software.
- Run `npm i` to install **dependencies**.
- Enter your database credentials in `.env`.
- Run `npm start` for run it.
- Go /setup page for setup your forum.
- Go `/setup` page for setup your forum.
### Extra (If you are not use `setup` page)
Run `node util/reset` to **reset the database** for duplicate key errors, and run `node util/admin` for give admin perms to first member.
Edit `config.json` for default theme color (`black` or `white`) of users, and forum name, meta description, character limits, discord auth enabler, global ratelimit.
Edit `config.json` for default theme for users, forum name, meta description, character limits, discord auth enabler, global ratelimit etc.
### How to install theme:
- Copy your theme to `public/themes` folder.
- Copy your theme to `src/themes` folder.
Additional note for themes: If a theme has not got any .ejs file, it will use default theme's .ejs files. default theme is in themes folder, named as `common`.
### DISCORD AUTH:
`"discord_auth": "your_app_id"` in config.json.
Add your app secret to `.env` as `DISCORD_SECRET`.
`"discord_auth": true` in config.json.
Add your app secret and app id to `.env` as `DISCORD_SECRET` and `DISCORD_ID`.
Create a redirect url in discord developer portal:
`https://forum_url.com/auth/discord`
@ -39,14 +40,8 @@ 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
- Profile Message or DM
@ -59,11 +54,15 @@ Akf-forum has got an API for AJAX (fetch), other clients etc. And, you can learn
- change category name
- _id
- add support for transition around gravatar
- forum setup page rewrite and directly open a router
- login w/ email
- BETTER SETUP PAGE
- add used open source libraries to README.md
- send public to common/public
- user.ejs for per theme
- categori search title like thread search
### front-end
- working reset button
- better pagination
- text alling center body
- add a css file for CodeMirror in threads / send message ok
- old contents / titles add to forum interface
@ -92,4 +91,4 @@ threads:
- 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

View File

@ -16,7 +16,7 @@
"max": 25,
"windowMs": 60000
},
"discord_auth": "",
"discord_auth": false,
"default_thread_state": "OPEN",
"default_user_state": "ACTIVE",
"email_auth": false,

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "akf-forum",
"version": "5.2.0",
"version": "5.2.6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "akf-forum",
"version": "5.2.0",
"version": "5.2.6",
"license": "GPL-3.0-or-later",
"dependencies": {
"bcrypt": "^5.1.0",

View File

@ -1,8 +1,8 @@
{
"name": "akf-forum",
"version": "5.2.0",
"version": "5.2.6",
"description": "A Node.js based forum software",
"main": "index.js",
"main": "src/index.js",
"scripts": {
"start": "node .",
"test": "echo \"Error: no test specified\" && exit 1"

View File

@ -1,82 +0,0 @@
<% 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">
<% 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 (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>
</nav>
<% if (user.admin) { %>
<select>
<option selected>IP LIST</option>
<% for(const ip of member.ips) { %>
<option><%= ip %></option>
<% } %>
</select>
<% } %>
<% } %>

View File

@ -1,44 +0,0 @@
<div class="footer">
<% 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>
<% } %>
<a href="https://github.com/Akif9748/akf-forum" style="color: white;"> This website is powered by
<span style="color: #ffbf00;">akf-forum</span>
</a>
<div>
<span style="color:white">Coders</span> <br>
<div style="text-align:center;">
<a href="https://github.com/Akif9748/" style="color: #ffbf00;">Akif</a><br><a href="#" style="color:#ffbf00;">Tokmak</a>
</div>
</div>
</div>

View File

@ -1,45 +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>

View File

@ -1,72 +0,0 @@
<% if(user?.admin || user?.id === member.id){ %>
<details>
<summary class="btn-outline-primary">User Menu:</summary>
<% if (!member.discordID && discord && user?.id === member.id) { %>
<a href="<%=discord%>" class="btn-outline-primary">DC auth</a>
<% } else if(member.discordID && user?.id === member.id) { %>
<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>
<script type="module">
import request from "/js/request.js";
const form = document.getElementById("form");
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>
<% 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 (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>
<% } %>
<% if (user?.admin) {%>
<h2>IP adresses of the user:</h2>
<select>
<% for(const ip of member.ips) { %>
<option><%= ip %></option>
<% } %>
</select>
<% } %>
</details>
<% } %>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,45 +0,0 @@
const { UserModel, ThreadModel, MessageModel } = require("../models")
const { Router } = require("express");
const app = Router();
const fs = require("fs");
app.get("/", async (req, res) => {
const
mem = process.memoryUsage().heapUsed / Math.pow(2, 20),
users = await UserModel.count({ deleted: false }),
threads = await ThreadModel.count({ state: "OPEN" }),
messages = await MessageModel.count({ deleted: false });
res.reply("index", { mem, users, threads, messages })
})
app.get("/setup", async (req, res) => {
if (await UserModel.exists({ admin: true })) return res.error(400, "You have already setuped the site.");
res.reply("setup");
})
app.post("/setup", async (req, res) => {
if (await UserModel.exists({ admin: true })) return res.error(400, "You have already setuped the site.");
let original = {};
try {
original = JSON.parse(fs.readFileSync("./config.json", "utf8"));
} catch (e) {
try {
original = JSON.parse(fs.readFileSync("./config.json.example", "utf8"));
} catch (e) { }
}
const content = req.body;
for (const key in content)
if (key in original && content[key])
original[key] = content[key];
fs.writeFileSync("./config.json", JSON.stringify(original,null,4));
require.cache[require.resolve("../config.json")] = require("../config.json");
res.redirect("/register");
})
module.exports = app;

View File

@ -1,6 +1,6 @@
require("dotenv").config();
const
{ def_theme, forum_name, description, limits, global_ratelimit: RLS, discord_auth, host } = require("./config.json"),
{ def_theme, forum_name, description, limits, global_ratelimit: RLS, discord_auth, host } = require("../config.json"),
{ UserModel, BanModel } = require("./models"),
port = process.env.PORT || 3000,
mongoose = require("mongoose"),
@ -23,7 +23,11 @@ app.ips = [];
app.set("view engine", "ejs");
app.set("limits", limits);
app.use(express.static("public"), express.json(), express.urlencoded({ extended: true }), IP(),
for (const theme of fs.readdirSync(join(__dirname, "themes")))
app.use(`/themes/${theme}`, express.static(join(__dirname, "themes", theme, "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.");
@ -37,15 +41,19 @@ app.use(express.static("public"), express.json(), express.urlencoded({ extended:
if (!themes.some(t => t.codename === theme.codename))
theme = def_theme;
res.reply = (page, options = {}, status = 200) => res.status(status).render(page, {
dataset: {
themes, theme, forum_name, description,
getFile: file => join(__dirname, "public", "themes", file),
},
user: req.user,
...options
});
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`);
return res.status(status).render(renderpage, {
dataset: {
themes, theme, forum_name, description,
getFile: file => join(__dirname, "themes", file),
},
user: req.user,
...options
});
}
res.error = (type, error) => res.reply("error", { type, error }, type);
@ -60,14 +68,14 @@ app.use(express.static("public"), express.json(), express.urlencoded({ extended:
}
);
if (discord_auth)
app.set("discord_auth", `https://discord.com/api/oauth2/authorize?client_id=${discord_auth}&redirect_uri=${host}%2Fauth%2Fdiscord&response_type=code&scope=identify`);
if (RLS.enabled) app.use(RL(RLS.windowMs, RLS.max));
for (const file of fs.readdirSync("./routes"))
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`);
for (const file of fs.readdirSync(join(__dirname, "routes")))
app.use("/" + file.replace(".js", ""), require(`./routes/${file}`));
app.all("*", (req, res) => res.error(404, "This page does not exist on this forum."));
app.listen(port, () => console.log(`${forum_name}-forum on port:`, port));
app.listen(port, () => console.log(`${forum_name} on port:`, port));

View File

@ -1,12 +1,12 @@
const RL = require('express-rate-limit');
const nodemailer = require("nodemailer");
const config = require("./config.json");
const config = require("../config.json");
const crypto = require("crypto");
const { readdirSync } = require('fs');
const { join } = require('path');
require("dotenv").config();
module.exports = {
themes: readdirSync("./public/themes").filter(f => f !== "common").map(f => require(`./public/themes/${f}`)),
themes: readdirSync(join(__dirname, "themes")).filter(f => f !== "common").map(f => require(`./themes/${f}`)),
threadEnum: ["OPEN", "APPROVAL", "DELETED"],
userEnum: ["ACTIVE", "APPROVAL", "DELETED", "BANNED"],
RL(windowMs = 60_000, max = 1) {

View File

@ -1,6 +1,6 @@
const mongoose = require("mongoose");
const cache = require("./cache");
const { limits } = require("../config.json");
const { limits } = require("../../config.json");
const schema = new mongoose.Schema({
id: { type: String, unique: true },

View File

@ -1,7 +1,7 @@
const mongoose = require("mongoose");
const cache = require("./cache")
const MessageModel = require("./Message");
const { limits, default_thread_state } = require("../config.json");
const { limits, default_thread_state } = require("../../config.json");
const { threadEnum } = require("../lib");
const schema = new mongoose.Schema({
id: { type: String, unique: true },

View File

@ -1,5 +1,5 @@
const mongoose = require("mongoose")
const { def_theme, limits, default_user_state } = require("../config.json");
const { def_theme, limits, default_user_state } = require("../../config.json");
const { userEnum } = require("../lib");
const schema = new mongoose.Schema({

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

17
src/routes/.js Normal file
View File

@ -0,0 +1,17 @@
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 });
res.reply("index", { categories, users, threads, messages, newestMember: newestMember.name });
});
module.exports = app;

View File

@ -3,7 +3,7 @@ const app = Router();
const fs = require("fs");
const bcrypt = require("bcrypt");
const { UserModel } = require("../../models");
const{join}=require("path");
app.use(async (req, res, next) => {
res.error = (status, error) => res.status(status).json({ error });
@ -31,7 +31,7 @@ app.use(async (req, res, next) => {
app.get("/me", (req, res) => res.complate(req.user))
for (const file of fs.readdirSync("./routes/api/routes"))
for (const file of fs.readdirSync(join(__dirname, "routes")))
app.use("/" + file.replace(".js", ""), require(`./routes/${file}`));
app.all("*", (req, res) => res.error(400, "Bad request"));

View File

@ -16,10 +16,10 @@ app.get("/", (req, res) => {
}
});
app.put("/", (req, res) => {
const write= req.query.text ? req.body : JSON.stringify(req.body, null, 4)
fs.writeFileSync("./config.json",write );
require.cache[require.resolve("../../../config.json")] = require("../../../config.json");
res.complate(require("../../../config.json"));
const write = req.query.text ? req.body : JSON.stringify(req.body, null, 4)
fs.writeFileSync("./config.json", write);
require.cache[require.resolve("../../../../config.json")] = require("../../../../config.json");
res.complate(require("../../../../config.json"));
});
module.exports = app;

View File

@ -3,7 +3,7 @@ const { Router } = require("express");
const multer = require("multer");
const { themes } = require("../../../lib")
const app = Router();
const { join } = require("path");
app.param("id", async (req, res, next, id) => {
req.member = await UserModel.get(id, req.user.admin ? "+lastSeen +ips" : "");
@ -80,7 +80,7 @@ app.post("/:id/ban", async (req, res) => {
});
const storage = multer.diskStorage({
destination:'./public/images/avatars',
destination: join(__dirname, "..", "..", "..", "public", "images", "avatars"),
filename: function (req, _file, cb) {
cb(null, req.member.id + ".jpg")
}
@ -89,7 +89,7 @@ const storage = multer.diskStorage({
const upload = multer({ storage })
app.post("/:id/avatar", upload.single('avatar'), async (req, res) => {
const { member } = req;
if (req.user.id !== member.id && !req.user.admin) return res.error(403, "You have not got permission for this.");

View File

@ -2,10 +2,10 @@ const { Router } = require("express")
const { UserModel } = require("../models");
const fetch = require("node-fetch");
const app = Router();
const { host, discord_auth, email_auth } = require("../config.json")
const { host, email_auth } = require("../../config.json")
app.get("/discord", async (req, res) => {
const client_id = discord_auth;
const client_id = process.env.DISCORD_ID;
if (!client_id) return res.error(404, "Discord auth is disabled")
const { code } = req.query;
if (!code) return res.error(400, "No code provided");

View File

@ -3,7 +3,7 @@ const { Router } = require("express");
const app = Router();
const bcrypt = require("bcrypt");
app.get("/", (req, res) => res.reply("login", { redirect: req.query.redirect, user: null, discord: req.app.get("discord_auth") }));
app.get("/", (req, res) => res.reply("login", { redirect: req.query.redirect, user: null, discord: req.app.get("DISCORD_AUTH_URL") }));
app.post("/", async (req, res) => {
req.session.userID = null;

View File

@ -3,8 +3,8 @@ const { Router } = require("express")
const bcrypt = require("bcrypt");
const { RL, transporter, emailRegEx, getGravatar } = require('../lib');
const app = Router();
const { email_auth, forum_name, host } = require("../config.json");
app.get("/", (req, res) => res.reply("register", { user: null, discord: req.app.get("discord_auth"), mail: email_auth }));
const { email_auth, forum_name, host } = require("../../config.json");
app.get("/", (req, res) => res.reply("register", { user: null, discord: req.app.get("DISCORD_AUTH_URL"), mail: email_auth }));
app.post("/", RL(24 * 60 * 60_000, 5), async (req, res) => {

34
src/routes/setup.js Normal file
View File

@ -0,0 +1,34 @@
const { UserModel } = require("../models")
const { Router } = require("express");
const app = Router();
const fs = require("fs");
app.use(async (req, res, next) => {
if (await UserModel.exists({ admin: true })) return res.error(400, "You have already setuped the site.");
next();
});
app.get("/", async (req, res) => res.reply("setup"))
app.post("/", async (req, res) => {
let original = {};
try {
original = JSON.parse(fs.readFileSync("./config.json", "utf8"));
} catch (e) {
try {
original = JSON.parse(fs.readFileSync("./config.json.example", "utf8"));
// eslint-disable-next-line no-empty
} catch (e) { }
}
const content = req.body;
for (const key in content)
if (key in original && content[key])
original[key] = content[key];
fs.writeFileSync("./config.json", JSON.stringify(original, null, 4));
require.cache[require.resolve("../../config.json")] = require("../../config.json");
res.redirect("/register");
})
module.exports = app;

View File

@ -28,7 +28,7 @@ app.get("/:id", async (req, res) => {
const message = await MessageModel.count({ authorID: id });
const thread = await ThreadModel.count({ authorID: id });
res.reply("user", { member, counts: { message, thread }, discord: req.app.get("discord_auth") })
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

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

View File

@ -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">&times;</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>

View File

@ -0,0 +1,74 @@
<!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" 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>
<% } %>
<% for(let i=0; i < pages; i++){ %>
<li class="page-item <%= i==page?'active':'' %>"><a href="/categories?page=<%= i %>" class="page-link"><%= 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>
<% } %>
</ul>
</nav>
</div>
<% } %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>
</html>

View File

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

View File

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

View File

@ -0,0 +1,124 @@
<!DOCTYPE html>
<html lang="en">
<%- include("extra/meta", {title: "Welcome to the "+dataset.forum_name+"-forum!" }) %>
<body>
<%- include("extra/navbar") %>
<div class="container my-3">
<nav class="breadcrumb">
<span class="breadcrumb-item active">
<% if (user) { %>
Welcome, <%= user.name %>
<% } else { %>
Welcome, Guest! <a href="/register">You can press 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>
<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>
</tr>
</thead>
<tbody>
<tr class="thread">
<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>
</td>
<td>
<div>5</div>
</td>
<td>
<div>18</div>
</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>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-12 col-xl-3">
<aside>
<div class="row">
<div class="col-12 col-sm-6 col-xl-12">
<div class="card mb-3 mb-sm-0 mb-xl-3">
<div class="card-body">
<h2 class="h4 card-title">Members online</h2>
<ul class="list-unstyled mb-0">
<li><a href="/users">You</a></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>
</dl>
<dl class="row mb-0">
<dt class="col-8">Members:</dt>
<dd class="col-4 mb-0">-</dd>
</dl>
<dl class="row mb-0">
<dt class="col-8">Guests:</dt>
<dd class="col-4 mb-0">-</dd>
</dl>
</div>
</div>
</div>
<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>
<dd class="col-4 mb-0"><%= categories %></dd>
</dl>
<dl class="row mb-0">
<dt class="col-8">Total threads:</dt>
<dd class="col-4 mb-0"><%= threads %></dd>
</dl>
<dl class="row mb-0">
<dt class="col-8">Total messages:</dt>
<dd class="col-4 mb-0"><%= messages %></dd>
</dl>
<dl class="row mb-0">
<dt class="col-8">Total members:</dt>
<dd class="col-4 mb-0"><%= users %></dd>
</dl>
</div>
<div class="card-footer">
<div>Newest member:</div>
<div><a href="#0"><%= newestMember %></a></div>
</div>
</div>
</div>
</div>
</aside>
</div>
</div>
</div>
<%- include("extra/footer") %>
</body>
</html>

View File

@ -0,0 +1,168 @@
<!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="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/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" 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>
<% } %>
<% 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 %>
<% 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>
<% } %>
</ul>
</nav>
</div>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>
</html>

View File

@ -0,0 +1,83 @@
<!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> </div>
<div class="avatar">by <a href="<%= thread.getLink() %>"><%= 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" 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>
<% } %>
<% for(let i=0; i < pages; i++){ %>
<li class="page-item <%= i==page?'active':'' %>"><a href="/threads?page=<%= i %>" class="page-link"><%= 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>
<% } %>
</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>

View File

@ -0,0 +1,164 @@
<!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="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">
<% 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">
<% 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 (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>
</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>
<% if (member.admin) { %>
<h2 class="userbox-value" style="align-self: center;">Admin</h2>
<% } %>
<% 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>
<% } %>
<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>
</html>

View File

@ -0,0 +1,51 @@
<!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" 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>
<% } %>
<% for(let i=0; i < pages; i++){ %>
<li class="page-item <%= i==page?'active':'' %>"><a href="/categories?page=<%= i %>" class="page-link"><%= 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>
<% } %>
</ul>
</nav>
</div>
<% } %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>
</html>

View File

@ -1,11 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<%- include("extra/meta", {title: "Admin Panel!" }) %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Admin Panel!" }) %>
<body style="text-align: center;">
<%- include("extra/navbar") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
<style>
table {
font-family: arial, sans-serif;
@ -83,7 +84,7 @@
}
</script>
</div>
<%- include("extra/footer") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<%- include("extra/meta", {title: "Avatar Upload Panel!" }) %>
<%- 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">
@ -27,7 +27,8 @@
</style>
<script src="/libs/cropper/cropper.js"></script>
<%- include("extra/navbar") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
<div class="container">
@ -107,7 +108,7 @@
});
</script>
<%- include("extra/footer") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>

View File

@ -1,11 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<%- include("extra/meta", {title: "Thread list!" }) %>
<%- 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("extra/navbar") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
<div class="threads">
<% categories.forEach(category=>{ %>
<a href="<%= category.getLink() %>">
@ -48,7 +49,7 @@
</div>
<% } %>
<%- include("extra/footer") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>

View File

@ -1,10 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<%- include("extra/meta", {title: "Edit Forum Config!" }) %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Edit Forum Config!" }) %>
<body style="text-align: center;">
<%- include("extra/navbar") %>
<%- 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>
@ -24,7 +25,7 @@
}
</script>
<%- include("extra/footer") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>

View File

@ -1,9 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<%- include("extra/meta", {title: "Create Category!" }) %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Create Category!" }) %>
<body>
<%- include("extra/navbar") %>
<%- 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>
@ -41,7 +42,7 @@
});
</script>
<%- include("extra/footer") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>

View File

@ -1,11 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<%- include("extra/meta", {title: "Create thread!" }) %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Create thread!" }) %>
<body >
<%- include("extra/navbar") %>
<%- 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>
@ -53,7 +54,7 @@
});
</script>
<%- include("extra/footer") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>

View File

@ -1,11 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<%- include("extra/meta", {title: member.name }) %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: member.name }) %>
<body>
<%- include("extra/navbar") %>
<%- 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>
@ -47,7 +48,7 @@
location.reload();
});
</script>
<%- include("extra/footer") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>

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

@ -0,0 +1,44 @@
<div class="footer">
<% 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>
<% } %>
<a href="https://github.com/Akif9748/akf-forum" style="color: white;"> This website is powered by
<span style="color: #ffbf00;">akf-forum</span>
</a>
<div>
<span style="color:white">Coders</span> <br>
<div style="text-align:center;">
<a href="https://github.com/Akif9748/" style="color: #ffbf00;">Akif</a><br><a href="#" style="color:#ffbf00;">Tokmak</a>
</div>
</div>
</div>

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

View File

@ -1,13 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<%- include("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+"-forum!" }) %>
<body>
<link rel="stylesheet" href="/css/user.css" />
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
<%- include("extra/navbar") %>
<div class="usercontent">
<% if (user) { %>
@ -44,12 +43,12 @@
</div>
<div class="userbox">
<h2 class="userbox-title">Memory usage:</h2>
<h2 class="userbox-value"><%= mem.toFixed(2); %> MB</h2>
<h2 class="userbox-title">Category count:</h2>
<h2 class="userbox-value"><%= categories %></h2>
</div>
</div>
<%- include("extra/footer") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>

View File

@ -1,11 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<%- include("extra/meta", {title: "Log in!" }) %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Log in!" }) %>
<body style="text-align: center;">
<%- include("extra/navbar") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
<h1 class="title">Login</h1>
@ -20,7 +21,7 @@
<% } %>
<a href="/register" class="btn-outline-primary">Register</a>
<%- include("extra/footer") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>

View File

@ -1,11 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<%- include("extra/meta", {title:"Message search!" }) %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title:"Message search!" }) %>
<body style="text-align: center;">
<%- include("extra/navbar") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
<div id="messages">
@ -27,7 +28,7 @@
<% }); %>
</div>
<%- include("extra/footer") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>

View File

@ -1,12 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<%- include("extra/meta", {title: "Register!" }) %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Register!" }) %>
<body style="text-align: center;">
<%- include("extra/navbar") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
<h1 class="title">Register</h1>
@ -24,7 +25,7 @@
<a href="/login" class="btn-outline-primary">Login</a>
<%- include("extra/footer") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>

View File

@ -1,11 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<%- include("extra/meta", {title: "Search page!" }) %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Search page!" }) %>
<body style="text-align: center;">
<%- include("extra/navbar") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
<h1 class="title">Search</h1>
@ -27,7 +28,7 @@
<input type="submit" value="Search" class="btn-primary" />
</form>
</div>
<%- include("extra/footer") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>

View File

@ -1,11 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<%- include("extra/meta", {title: "Setup the Akf-forum!" }) %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Setup the Akf-forum!" }) %>
<body style="text-align: center;">
<%- include("extra/navbar") %>
<%- 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>
@ -21,10 +22,7 @@
<input class="input" type="text" name="default_thread_state" value="ACTIVE" required>
Domain of the forum, defaulty setted:
<input class="input" type="text" name="host" id="domain" value="Akf-forum!" required>
<hr>
(Optional) Discord app ID for Discord login:
<input class="input" type="text" name="discord_auth">
<input type="submit" class="btn-primary" value="Setup">
</form>
<script>

View File

@ -1,11 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<%- include("extra/meta", {title: thread.title }) %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: thread.title }) %>
<body>
<%- include("extra/navbar") %>
<%- 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>
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
<link rel="stylesheet" href="/libs/simplemde/simplemde.min.css">
@ -159,7 +160,7 @@
</div>
</div>
<%- include("extra/footer") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>

View File

@ -1,11 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<%- include("extra/meta", {title: "Thread list!" }) %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Thread list!" }) %>
<body style="color: var(--anti); text-align: center;">
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
<%- include("extra/navbar") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
<h1><%= title || "Threads" %></h1>
<h2><%= desp %></h2>
<div class="threads">
@ -54,7 +55,7 @@
</div>
<% } %>
<%- include("extra/footer") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>

View File

@ -0,0 +1,151 @@
<!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="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">
<% if(user?.admin || user?.id === member.id){ %>
<details>
<summary class="btn-outline-primary">User Menu:</summary>
<% if (!member.discordID && discord && user?.id === member.id) { %>
<a href="<%=discord%>" class="btn-outline-primary">DC auth</a>
<% } else if(member.discordID && user?.id === member.id) { %>
<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" href="/users/<%=member.id%>/edit" id="toogle">Edit user!</a>
<script type="module">
import request from "/js/request.js";
const form = document.getElementById("form");
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()
}
});
</script>
<% 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 (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>
<% } %>
<% if (user?.admin) {%>
<h2>IP adresses of the user:</h2>
<select>
<% for(const ip of member.ips) { %>
<option><%= ip %></option>
<% } %>
</select>
<% } %>
</details>
<% } %>
<div class="userbox" style="justify-content:center;">
<img style="width:150px;height:150px;border-radius:50%;" src="<%=member.avatar %>">
</div>
<% if (member.admin) { %>
<h2 class="userbox-value" style="align-self: center;">Admin</h2>
<% } %>
<% 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>
<% } %>
<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>
</html>

View File

@ -1,13 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<%- include("extra/meta", {title: "User list!" }) %>
<%- 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("extra/navbar") %>
<%- 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;">
@ -43,7 +44,7 @@
</div>
<% } %>
<%- include("extra/footer") %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>

View File

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

Some files were not shown because too many files have changed in this diff Show More