mirror of
https://github.com/Akif9748/akf-forum.git
synced 2024-11-23 04:10:40 +03:00
Compare commits
2 commits
bd722f1651
...
16a2504665
Author | SHA1 | Date | |
---|---|---|---|
16a2504665 | |||
93bb17981f |
44 changed files with 849 additions and 254 deletions
|
@ -1,3 +1,6 @@
|
||||||
MONGO_DB_URL = mongodb://localhost:27017/akf-forum
|
MONGO_DB_URL = mongodb://localhost:27017/akf-forum
|
||||||
SECRET = secret
|
SECRET = secret
|
||||||
DISCORD_SECRET = yourDiscordSecret
|
DISCORD_SECRET = yourDiscordSecret
|
||||||
|
EMAIL_USER =
|
||||||
|
EMAIL_PASS =
|
||||||
|
EMAIL_SERVICE =
|
20
README.md
20
README.md
|
@ -12,6 +12,9 @@ A Node.js based forum software.
|
||||||
Run `node util/reset` to **reset the database** for duplicate key errors, and run `node util/admin` for give admin perms to first member.
|
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 color (`black` or `white`) of users, and forum name, meta description, character limits, discord auth enabler, global ratelimit.
|
||||||
|
|
||||||
|
### How to install theme:
|
||||||
|
- Copy your theme to `public/themes` folder.
|
||||||
|
|
||||||
### DISCORD AUTH:
|
### DISCORD AUTH:
|
||||||
`"discord_auth": "your_app_id"` in config.json.
|
`"discord_auth": "your_app_id"` in config.json.
|
||||||
Add your app secret to `.env` as `DISCORD_SECRET`.
|
Add your app secret to `.env` as `DISCORD_SECRET`.
|
||||||
|
@ -56,22 +59,17 @@ Akf-forum has got an API for AJAX (fetch), other clients etc. And, you can learn
|
||||||
- change category name
|
- change category name
|
||||||
- _id
|
- _id
|
||||||
- add support for transition around gravatar
|
- add support for transition around gravatar
|
||||||
- forum setup page++
|
- forum setup page rewrite and directly open a router
|
||||||
|
|
||||||
### theme to do:
|
|
||||||
- add bootstrap for navbar
|
|
||||||
- routes/api/routes/users.js check,
|
|
||||||
themes/default/extra/footer.ejs check,
|
|
||||||
themes/default/extra/meta.ejs check
|
|
||||||
- add theme support again, but only works with css folder. Put every css file into one file. (themes/default/css/main.css)
|
|
||||||
|
|
||||||
### front-end
|
### front-end
|
||||||
- better usermenu for user profile
|
- add a css file for CodeMirror in threads / send message ok
|
||||||
- old contents / titles add to forum interface
|
- old contents / titles add to forum interface
|
||||||
- categories page is need a update, thread count in category (?)
|
- categories page is need a update, thread count in category (?)
|
||||||
- add ban button to user profile.?
|
- add ban button to user profile
|
||||||
- who liked a message for web.
|
- who liked a message
|
||||||
- give admin button, not is admin
|
- give admin button, not is admin
|
||||||
|
- edit user ++
|
||||||
|
- rewrite main page, list new messages
|
||||||
|
|
||||||
## Major Version History
|
## Major Version History
|
||||||
- V4: Caching
|
- V4: Caching
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"def_theme": {
|
"def_theme": {
|
||||||
"name": "white",
|
"codename": "bootstrap_black",
|
||||||
"language": "en"
|
"language": "en"
|
||||||
},
|
},
|
||||||
"forum_name": "akf",
|
"forum_name": "akf",
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
},
|
},
|
||||||
"discord_auth": "",
|
"discord_auth": "",
|
||||||
"default_thread_state": "OPEN",
|
"default_thread_state": "OPEN",
|
||||||
"email_auth": false,
|
|
||||||
"default_user_state": "ACTIVE",
|
"default_user_state": "ACTIVE",
|
||||||
|
"email_auth": false,
|
||||||
"host": "https://akf-forum.glitch.me"
|
"host": "https://akf-forum.glitch.me"
|
||||||
}
|
}
|
14
index.js
14
index.js
|
@ -6,10 +6,10 @@ const
|
||||||
mongoose = require("mongoose"),
|
mongoose = require("mongoose"),
|
||||||
express = require('express'),
|
express = require('express'),
|
||||||
fs = require("fs"),
|
fs = require("fs"),
|
||||||
|
{ join } = require("path"),
|
||||||
app = express(),
|
app = express(),
|
||||||
{ mw: IP } = require('request-ip'),
|
{ mw: IP } = require('request-ip'),
|
||||||
{ RL } = require('./lib'),
|
{ RL, themes } = require('./lib'),
|
||||||
{ themes } = require("./config.json"),
|
|
||||||
SES = require('express-session'),
|
SES = require('express-session'),
|
||||||
MS = require("connect-mongo"),
|
MS = require("connect-mongo"),
|
||||||
DB = mongoose.connect(process.env.MONGO_DB_URL)
|
DB = mongoose.connect(process.env.MONGO_DB_URL)
|
||||||
|
@ -32,13 +32,15 @@ app.use(express.static("public"), express.json(), express.urlencoded({ extended:
|
||||||
lastSeen: Date.now(), $addToSet: { ips: req.clientIp }
|
lastSeen: Date.now(), $addToSet: { ips: req.clientIp }
|
||||||
}) : null;
|
}) : null;
|
||||||
|
|
||||||
|
let theme = req.user?.theme || def_theme;
|
||||||
|
|
||||||
|
if (!themes.some(t => t.codename === theme.codename))
|
||||||
|
theme = def_theme;
|
||||||
|
|
||||||
res.reply = (page, options = {}, status = 200) => res.status(status).render(page, {
|
res.reply = (page, options = {}, status = 200) => res.status(status).render(page, {
|
||||||
dataset: {
|
dataset: {
|
||||||
themes,
|
themes, theme, forum_name, description,
|
||||||
theme: req.user?.theme || def_theme,
|
getFile: file => join(__dirname, "public", "themes", file),
|
||||||
forum_name,
|
|
||||||
description
|
|
||||||
},
|
},
|
||||||
user: req.user,
|
user: req.user,
|
||||||
...options
|
...options
|
||||||
|
|
2
lib.js
2
lib.js
|
@ -2,9 +2,11 @@ const RL = require('express-rate-limit');
|
||||||
const nodemailer = require("nodemailer");
|
const nodemailer = require("nodemailer");
|
||||||
const config = require("./config.json");
|
const config = require("./config.json");
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
|
const { readdirSync } = require('fs');
|
||||||
|
|
||||||
require("dotenv").config();
|
require("dotenv").config();
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
themes: readdirSync("./public/themes").filter(f => f !== "common").map(f => require(`./public/themes/${f}`)),
|
||||||
threadEnum: ["OPEN", "APPROVAL", "DELETED"],
|
threadEnum: ["OPEN", "APPROVAL", "DELETED"],
|
||||||
userEnum: ["ACTIVE", "APPROVAL", "DELETED", "BANNED"],
|
userEnum: ["ACTIVE", "APPROVAL", "DELETED", "BANNED"],
|
||||||
RL(windowMs = 60_000, max = 1) {
|
RL(windowMs = 60_000, max = 1) {
|
||||||
|
|
|
@ -12,7 +12,7 @@ const schema = new mongoose.Schema({
|
||||||
about: { type: String, default: "", maxlength: limits.desp },
|
about: { type: String, default: "", maxlength: limits.desp },
|
||||||
admin: { type: Boolean, default: false },
|
admin: { type: Boolean, default: false },
|
||||||
theme: {
|
theme: {
|
||||||
name: { type: String, default: def_theme.name },
|
codename: { type: String, default: def_theme.codename },
|
||||||
language: { type: String, default: def_theme.language }
|
language: { type: String, default: def_theme.language }
|
||||||
},
|
},
|
||||||
lastSeen: { type: Date, default: Date.now, select: false },
|
lastSeen: { type: Date, default: Date.now, select: false },
|
||||||
|
|
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "akf-forum",
|
"name": "akf-forum",
|
||||||
"version": "4.22.1",
|
"version": "5.0.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "akf-forum",
|
"name": "akf-forum",
|
||||||
"version": "4.22.1",
|
"version": "5.0.0",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "akf-forum",
|
"name": "akf-forum",
|
||||||
"version": "4.22.1",
|
"version": "5.0.0",
|
||||||
"description": "A Node.js based forum software",
|
"description": "A Node.js based forum software",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -22,7 +22,7 @@ form {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* MODAL */
|
/* MODAL */
|
||||||
.modal {
|
.forum-modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -32,7 +32,7 @@ form {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.forum-modal-content {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
@ -44,12 +44,12 @@ form {
|
||||||
animation: fadeIn 1s forwards;
|
animation: fadeIn 1s forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal.no-active {
|
.forum-modal.no-active {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-close {
|
.forum-modal-close {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
|
@ -69,7 +69,7 @@ form {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media(max-width:760px) {
|
@media(max-width:760px) {
|
||||||
.modal-content {
|
.forum-modal-content {
|
||||||
width: 95%;
|
width: 95%;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -80,7 +80,7 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50px;
|
top: 50px;
|
||||||
right: 0;
|
right: 0;
|
||||||
background-color: var(--btn-clr-1);
|
background-color: var(--second);
|
||||||
width: 100px;
|
width: 100px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: none;
|
display: none;
|
||||||
|
|
10
public/themes/bootstrap_black/bootstrap-night.min.css
vendored
Normal file
10
public/themes/bootstrap_black/bootstrap-night.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
public/themes/bootstrap_black/bootstrap.min.js
vendored
Normal file
7
public/themes/bootstrap_black/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
41
public/themes/bootstrap_black/extra/footer.ejs
Normal file
41
public/themes/bootstrap_black/extra/footer.ejs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<footer class="text-center text-white fixed-bottom">
|
||||||
|
<% 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>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<div class="text-center p-3">
|
||||||
|
akf-forum bootstrap theme created by <a class="text-white" href="https://akif9748.github.io/">Akif9748</a>,<br>
|
||||||
|
This website is powered by <a class="text-white" href="https://github.com/Akif9748/akf-forum">akf-forum</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
10
public/themes/bootstrap_black/extra/meta.ejs
Normal file
10
public/themes/bootstrap_black/extra/meta.ejs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title><%= title || dataset.forum_name +"-forum" %></title>
|
||||||
|
<meta name="description" content="<%= dataset.description %>">
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||||
|
<link href="/themes/bootstrap_black/bootstrap-night.min.css" rel="stylesheet">
|
||||||
|
<script src="/themes/bootstrap_black/bootstrap.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="/themes/bootstrap_black/main.css" />
|
||||||
|
</head>
|
72
public/themes/bootstrap_black/extra/navbar.ejs
Normal file
72
public/themes/bootstrap_black/extra/navbar.ejs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
<% if (user?.admin){ %>
|
||||||
|
<div class="admin-bar">
|
||||||
|
<a href="/admin" class="admin-bar">Click here to reach admin panel</a>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
|
||||||
|
<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="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a class="navbar-brand" href="/"><%= dataset.forum_name.toUpperCase() %><span>-FORUM</span></a>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="navbarText">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/threads/create/">Create Thread</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/categories">Categories</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/threads">Threads</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/users">Users</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/search">Search</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/login">Logout</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<% if (user) { %>
|
||||||
|
<a class="navbar-brand" href="<%=user.getLink()%>"> <%= user.name %>
|
||||||
|
<img style="border-radius: 50%;" width="32" src="<%=user.avatar %>">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<% } else{ %>
|
||||||
|
<a class="navbar-brand" id="login" href="/login">Login</a>
|
||||||
|
<script>
|
||||||
|
document.getElementById("login").href += "?redirect=" + location.pathname;
|
||||||
|
</script>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const menuItems = document.getElementsByClassName("nav-link");
|
||||||
|
|
||||||
|
for (let i = 0; i < menuItems.length; i++){console.log(menuItems[i].getAttribute("href"))
|
||||||
|
if (window.location.pathname.includes(menuItems[i].getAttribute("href"))) {
|
||||||
|
menuItems[i].classList.add("active");
|
||||||
|
//break;
|
||||||
|
}}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</nav>
|
82
public/themes/bootstrap_black/extra/usermenu.ejs
Normal file
82
public/themes/bootstrap_black/extra/usermenu.ejs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<% 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>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% } %>
|
6
public/themes/bootstrap_black/index.js
Normal file
6
public/themes/bootstrap_black/index.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
name: "Bootstrap black theme",
|
||||||
|
codename: "bootstrap_black",
|
||||||
|
description: "A black theme fueled by bootstrap, created by Akif9748 and overwrited Alair's website's theme.",
|
||||||
|
author: "Akif9748"
|
||||||
|
}
|
273
public/themes/bootstrap_black/main.css
Normal file
273
public/themes/bootstrap_black/main.css
Normal file
|
@ -0,0 +1,273 @@
|
||||||
|
@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 {
|
||||||
|
--alair: #736FE9;
|
||||||
|
--main: #736FE9;
|
||||||
|
--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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***********************************
|
||||||
|
|
||||||
|
START OF OLD THEME
|
||||||
|
|
||||||
|
***********************************/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: Poppins;
|
||||||
|
min-height: 400px;
|
||||||
|
margin-bottom: 100px;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 28px;
|
||||||
|
color: var(--main);
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo>span {
|
||||||
|
color: var(--second);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
color: #fafafa;
|
||||||
|
background-color: var(--alair);
|
||||||
|
border-color: var(--alair);
|
||||||
|
padding: 10px 20px 10px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid var(--main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
color: var(--alair);
|
||||||
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
border: 2px solid var(--alair);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
color: var(--btn-clr-1);
|
||||||
|
background-color: var(--important);
|
||||||
|
padding: 0px 10px 0px 10px;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid var(--important);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.btn-outline-primary {
|
||||||
|
|
||||||
|
|
||||||
|
color: var(--alair);
|
||||||
|
padding: 10px 20px 10px 20px;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid var(--borders);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-primary:hover {
|
||||||
|
color: var(--btn-clr-1);
|
||||||
|
background-color: var(--alair);
|
||||||
|
border: 2px solid var(--alair);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background-color: var(--important);
|
||||||
|
border: 2px solid var(--important);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
padding: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
background-color: var(--second);
|
||||||
|
color: var(--menu-item);
|
||||||
|
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0px 10px 0px 0px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-menu {
|
||||||
|
background-color: var(--main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background-color: var(--main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-bar {
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--menu-item);
|
||||||
|
background: var(--main);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.avatar {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar>img {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-username {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--main);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
margin: 0px 10px 10px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.btn-outline-primary {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***********************************
|
||||||
|
|
||||||
|
START OF BOOTSTRAP THEME
|
||||||
|
|
||||||
|
***********************************/
|
||||||
|
|
||||||
|
.a:hover {
|
||||||
|
color: var(--alair);
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror {
|
||||||
|
color: var(--input-clr);
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
footer {
|
||||||
|
background-color: #000000;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 400px;
|
||||||
|
background-size: cover;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--alair);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alair-trans {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alair-cl {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.alair-cl {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-sm {
|
||||||
|
margin-top: 25px;
|
||||||
|
margin-bottom: 125px;
|
||||||
|
}
|
44
public/themes/common/footer.ejs
Normal file
44
public/themes/common/footer.ejs
Normal 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>
|
45
public/themes/common/navbar.ejs
Normal file
45
public/themes/common/navbar.ejs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
|
||||||
|
<% 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>
|
72
public/themes/common/usermenu.ejs
Normal file
72
public/themes/common/usermenu.ejs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
<% 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>
|
||||||
|
<% } %>
|
1
public/themes/default_black/extra/footer.ejs
Normal file
1
public/themes/default_black/extra/footer.ejs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<%- include(dataset.getFile("common/footer")) %>
|
9
public/themes/default_black/extra/meta.ejs
Normal file
9
public/themes/default_black/extra/meta.ejs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title><%= title || dataset.forum_name +"-forum" %></title>
|
||||||
|
<meta name="description" content="<%= dataset.description %>">
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||||
|
<link rel="stylesheet" href="/themes/default_black/main.css" />
|
||||||
|
<link rel="stylesheet" href="/css/common.css" />
|
||||||
|
</head>
|
1
public/themes/default_black/extra/navbar.ejs
Normal file
1
public/themes/default_black/extra/navbar.ejs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<%- include(dataset.getFile("common/navbar")) %>
|
1
public/themes/default_black/extra/usermenu.ejs
Normal file
1
public/themes/default_black/extra/usermenu.ejs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<%- include(dataset.getFile("common/usermenu")) %>
|
6
public/themes/default_black/index.js
Normal file
6
public/themes/default_black/index.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
name: "Default Black",
|
||||||
|
codename: "default_black",
|
||||||
|
description: "A black theme over the default white theme. Edited by Akif9748, Created by Tokmak.",
|
||||||
|
author: "Akif9748 & Tokmak"
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
|
/*
|
||||||
|
akf-forum black theme config file
|
||||||
|
*/
|
||||||
:root {
|
:root {
|
||||||
--main: #ac8fff;
|
--main: #ac8fff;
|
||||||
--btn-clr-1: #e8e8e8;
|
--btn-clr-1: #e8e8e8;
|
1
public/themes/default_white/extra/footer.ejs
Normal file
1
public/themes/default_white/extra/footer.ejs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<%- include(dataset.getFile("common/footer")) %>
|
9
public/themes/default_white/extra/meta.ejs
Normal file
9
public/themes/default_white/extra/meta.ejs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title><%= title || dataset.forum_name +"-forum" %></title>
|
||||||
|
<meta name="description" content="<%= dataset.description %>">
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||||
|
<link rel="stylesheet" href="/themes/default_white/main.css" />
|
||||||
|
<link rel="stylesheet" href="/css/common.css" />
|
||||||
|
</head>
|
1
public/themes/default_white/extra/navbar.ejs
Normal file
1
public/themes/default_white/extra/navbar.ejs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<%- include(dataset.getFile("common/navbar")) %>
|
1
public/themes/default_white/extra/usermenu.ejs
Normal file
1
public/themes/default_white/extra/usermenu.ejs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<%- include(dataset.getFile("common/usermenu")) %>
|
6
public/themes/default_white/index.js
Normal file
6
public/themes/default_white/index.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
name: "Default White",
|
||||||
|
codename: "default_white",
|
||||||
|
description: "Default white theme created by Tokmak.",
|
||||||
|
author: "Tokmak"
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
/* akf-forum default theme config file */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--main: #4d18e6;
|
--main: #4d18e6;
|
||||||
--btn-clr-1: #e8e8e8;
|
--btn-clr-1: #e8e8e8;
|
15
routes/api/routes/themes.js
Normal file
15
routes/api/routes/themes.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
const {themes}= require("../../../lib");
|
||||||
|
const { Router } = require("express")
|
||||||
|
|
||||||
|
const app = Router();
|
||||||
|
|
||||||
|
app.get("/", async (req, res) => res.complate(themes));
|
||||||
|
|
||||||
|
app.get("/:codename", async (req, res) => {
|
||||||
|
const theme = themes.find(t => t.codename === req.params.codename);
|
||||||
|
if (!theme) return res.error(404, "Theme not found.");
|
||||||
|
res.complate(theme);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = app;
|
|
@ -1,7 +1,7 @@
|
||||||
const { UserModel, BanModel } = require("../../../models");
|
const { UserModel, BanModel } = require("../../../models");
|
||||||
const { Router } = require("express");
|
const { Router } = require("express");
|
||||||
const multer = require("multer");
|
const multer = require("multer");
|
||||||
const { themes } = require("../../../config.json")
|
const { themes } = require("../../../lib")
|
||||||
const app = Router();
|
const app = Router();
|
||||||
|
|
||||||
app.param("id", async (req, res, next, id) => {
|
app.param("id", async (req, res, next, id) => {
|
||||||
|
@ -51,7 +51,7 @@ app.patch("/:id", async (req, res) => {
|
||||||
if (about.length > desp) return res.error(400, `About must be under ${desp} characters`);
|
if (about.length > desp) return res.error(400, `About must be under ${desp} characters`);
|
||||||
member.about = about;
|
member.about = about;
|
||||||
}
|
}
|
||||||
if (theme || themes.some(t => t.name === theme.name).includes(theme))
|
if (theme && themes.some(t => t.codename === theme.codename))
|
||||||
member.theme = theme;
|
member.theme = theme;
|
||||||
|
|
||||||
if (typeof admin === "boolean" || ["false", "true"].includes(admin)) member.admin = admin;
|
if (typeof admin === "boolean" || ["false", "true"].includes(admin)) member.admin = admin;
|
||||||
|
|
|
@ -18,6 +18,7 @@ app.get("/:id/avatar", async (req, res) => {
|
||||||
else
|
else
|
||||||
res.error(404, `We don't have any user with id ${req.params.id}.`);
|
res.error(404, `We don't have any user with id ${req.params.id}.`);
|
||||||
})
|
})
|
||||||
|
|
||||||
app.get("/:id", async (req, res) => {
|
app.get("/:id", async (req, res) => {
|
||||||
const user = req.user
|
const user = req.user
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
@ -33,4 +34,17 @@ app.get("/:id", async (req, res) => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get("/:id/edit", async (req, res) => {
|
||||||
|
const user = req.user
|
||||||
|
const { id } = req.params;
|
||||||
|
const member = await UserModel.get(id);
|
||||||
|
if (!member) return res.error(404, `We don't have any user with id ${id}.`);
|
||||||
|
if (user?.admin || user.id === member.id)
|
||||||
|
return res.reply("edit_user", { member });
|
||||||
|
|
||||||
|
res.error(403, "You have not got permission for this.");
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
44
views/edit_user.ejs
Normal file
44
views/edit_user.ejs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<%- include("extra/meta", {title: member.name }) %>
|
||||||
|
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<%- include("extra/navbar") %>
|
||||||
|
<h1 class="title" style="text-align:center;">
|
||||||
|
Edit <a href="/users/<%= member.id %>"><%= member.name %></a>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<form id="form" style="box-shadow:none">
|
||||||
|
<input type="text" name="name" placeholder="<%=member.name%>" class="input">
|
||||||
|
|
||||||
|
<textarea class="input" name="about" rows="4" cols="60" name="content" placeholder="<%=member.about%>"></textarea>
|
||||||
|
<% if (user?.admin){ %>
|
||||||
|
Is Admin? <input id='admin' type='checkbox' value='true' name='admin' <%=member.admin ? "checked": ""%>>
|
||||||
|
<input id='adminHidden' type='hidden' value='false' name='admin'>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<button class="btn-primary" style="width:100%;">Update User!</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import request from "/js/request.js";
|
||||||
|
|
||||||
|
document.getElementById("form").addEventListener("submit", async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById('adminHidden').disabled = document.getElementById("admin").checked;
|
||||||
|
|
||||||
|
const object = {};
|
||||||
|
new FormData(e.target).forEach((value, key) => object[key] = value);
|
||||||
|
|
||||||
|
const res = await request("/api/users/<%=member.id%>", "PATCH", object);
|
||||||
|
if (res) alert(`User is updated!`);
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<%- include("extra/footer") %>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -1,33 +1 @@
|
||||||
<div class="footer">
|
<%- include(dataset.getFile(dataset.theme.codename +"/extra/footer")) %>
|
||||||
<% if (user){ %>
|
|
||||||
<select id="theme_select">
|
|
||||||
<% for(const theme of dataset.themes){%>
|
|
||||||
<option value="<%= theme %>"><%= theme %> theme</option>
|
|
||||||
<% } %>
|
|
||||||
</select>
|
|
||||||
<script>
|
|
||||||
const theme_select = document.getElementById("theme_select");
|
|
||||||
theme_select.querySelector(`option[value=<%= user.theme.name %>]`).selected = true;
|
|
||||||
theme_select.addEventListener("change", async e => {
|
|
||||||
const name = e.target.value;
|
|
||||||
await fetch('/api/users/<%= user.id %>', {
|
|
||||||
method: 'PATCH',
|
|
||||||
body: JSON.stringify({
|
|
||||||
theme: {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
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>
|
|
|
@ -1,9 +1 @@
|
||||||
<head>
|
<%- include(dataset.getFile(dataset.theme.codename +"/extra/meta")) %>
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title><%= title || dataset.forum_name +"-forum" %></title>
|
|
||||||
<meta name="description" content="<%= dataset.description %>">
|
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
|
||||||
<link rel="stylesheet" href="/css/themes/<%= dataset.theme.name %>.css" />
|
|
||||||
<link rel="stylesheet" href="/css/common.css" />
|
|
||||||
</head>
|
|
|
@ -1,45 +1 @@
|
||||||
|
<%- include(dataset.getFile(dataset.theme.codename +"/extra/navbar")) %>
|
||||||
<% 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>
|
|
|
@ -9,21 +9,16 @@
|
||||||
|
|
||||||
<%- include("extra/navbar") %>
|
<%- include("extra/navbar") %>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<% if (user) { %>
|
<% if (user) { %>
|
||||||
<h2 style="color: var(--main);">
|
<h2 style="color: var(--main);">
|
||||||
<div class="box-username">Welcome, <%= user.name %>
|
<div class="box-username">Welcome, <%= user.name %>
|
||||||
<div class="avatar"><img src="<%=user.avatar %>"></div>
|
<div class="avatar"><img src="<%=user.avatar %>"></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
You can log out of the site here:
|
Main page will be rewritten soon, so stay tuned!
|
||||||
|
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<a href="/login" class="btn-outline-primary">LOGOUT</a>
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<h2 style="color: var(--main);">Welcome, Guest!<br>You can press the button to register:
|
<h2 style="color: var(--main);">Welcome, Guest!<br>You can press the button to register:
|
||||||
<a href="/register" class="btn-outline-primary">REGISTER</a>
|
<a href="/register" class="btn-outline-primary">REGISTER</a>
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
|
||||||
|
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
|
||||||
|
|
||||||
|
|
||||||
<%- include("extra/meta", {title: thread.title }) %>
|
<%- include("extra/meta", {title: thread.title }) %>
|
||||||
|
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<%- include("extra/navbar") %>
|
<%- include("extra/navbar") %>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/2.1.0/showdown.min.js"></script>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
|
|
||||||
|
|
||||||
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/css/thread.css" />
|
<link rel="stylesheet" href="/css/thread.css" />
|
||||||
<link rel="stylesheet" href="/css/pages.css" />
|
<link rel="stylesheet" href="/css/pages.css" />
|
||||||
|
@ -142,6 +143,13 @@
|
||||||
if (res) location.href = `/threads/<%= thread.id %>?page=${tp-1}`;
|
if (res) location.href = `/threads/<%= thread.id %>?page=${tp-1}`;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<style>
|
||||||
|
.fa {
|
||||||
|
color: var(--main);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<% }%>
|
<% }%>
|
||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
<div class="back">
|
<div class="back">
|
||||||
|
|
129
views/user.ejs
129
views/user.ejs
|
@ -13,118 +13,8 @@
|
||||||
|
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<% 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>
|
|
||||||
<link rel="stylesheet" href="/css/modal.css" />
|
|
||||||
|
|
||||||
<a class="btn-outline-primary" id="toogle">Edit user!</a>
|
|
||||||
|
|
||||||
<div class="modal no-active" id="user-edit">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-close">
|
|
||||||
<i id="toogle" class="fa-solid fa-square-xmark"></i>
|
|
||||||
</div>
|
|
||||||
<h1 class="title" style="text-align:center;">Edit <a class="see" href="/users/<%= member.id %>"><%= member.name %></a></h1>
|
|
||||||
<div class="content">
|
|
||||||
<form id="form" class="see" style="box-shadow:none">
|
|
||||||
<input type="text" name="name" placeholder="<%=member.name%>" class="input">
|
|
||||||
|
|
||||||
<textarea class="input" name="about" rows="4" cols="60" name="content" placeholder="<%=member.about%>"></textarea>
|
|
||||||
<% if (user?.admin){ %>
|
|
||||||
Is Admin? <input id='admin' type='checkbox' value='true' name='admin' <%=member.admin ? "checked": ""%>>
|
|
||||||
<input id='adminHidden' type='hidden' value='false' name='admin'>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<button class="btn-primary" style="width:100%;">Update User!</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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')
|
|
||||||
|
|
||||||
});
|
|
||||||
form.addEventListener("submit", async e => {
|
|
||||||
e.preventDefault();
|
|
||||||
document.getElementById('adminHidden').disabled = document.getElementById("admin").checked;
|
|
||||||
|
|
||||||
const object = {};
|
|
||||||
new FormData(e.target).forEach((value, key) => object[key] = value);
|
|
||||||
|
|
||||||
const res = await request("/api/users/<%=member.id%>", "PATCH", object);
|
|
||||||
if (res) alert(`User is updated!`);
|
|
||||||
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>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
|
<%- include(dataset.getFile(dataset.theme.codename +"/extra/usermenu")) %>
|
||||||
<div class="box" style="justify-content:center;">
|
<div class="box" style="justify-content:center;">
|
||||||
<img style="width:150px;height:150px;border-radius:50%;" src="<%=member.avatar %>">
|
<img style="width:150px;height:150px;border-radius:50%;" src="<%=member.avatar %>">
|
||||||
</div>
|
</div>
|
||||||
|
@ -133,7 +23,7 @@
|
||||||
<h2 class="box-value" style="align-self: center;">Admin</h2>
|
<h2 class="box-value" style="align-self: center;">Admin</h2>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
<% if (member.about?.length) { %>
|
||||||
<div class="box-value" id="about" style="
|
<div class="box-value" id="about" style="
|
||||||
margin: 10px auto;
|
margin: 10px auto;
|
||||||
box-shadow: 0 0 5px 0 var(--second);
|
box-shadow: 0 0 5px 0 var(--second);
|
||||||
|
@ -152,6 +42,9 @@ color: var(--anti);
|
||||||
const about = document.getElementById("about")
|
const about = document.getElementById("about")
|
||||||
about.innerHTML = converter.makeHtml(about.innerText);
|
about.innerHTML = converter.makeHtml(about.innerText);
|
||||||
</script>
|
</script>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<h2 class="box-title">Name:</h2>
|
<h2 class="box-title">Name:</h2>
|
||||||
|
@ -168,12 +61,16 @@ color: var(--anti);
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<a href="/search/messages?authorID=<%= member.id %>" class="box-title">Message:</a>
|
<h2 class="box-title">Message:</h2>
|
||||||
<a class="box-value"><%= counts.message %></a>
|
<a class="box-value" href="/search/messages?authorID=<%= member.id %>">
|
||||||
|
<%= counts.message %>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<a href="/search/threads?authorID=<%= member.id %>" class="box-title">Thread:</a>
|
<h2 class="box-title">Thread:</h2>
|
||||||
<h2 class="box-value"><%= counts.thread %></h2>
|
<a class="box-value" href="/search/threads?authorID=<%= member.id %>">
|
||||||
|
<%= counts.thread %>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%- include("extra/footer") %>
|
<%- include("extra/footer") %>
|
||||||
|
|
|
@ -10,15 +10,14 @@
|
||||||
<link rel="stylesheet" href="/css/pages.css" />
|
<link rel="stylesheet" href="/css/pages.css" />
|
||||||
|
|
||||||
<%- include("extra/navbar") %>
|
<%- include("extra/navbar") %>
|
||||||
|
|
||||||
<div class="users">
|
<div class="users">
|
||||||
<% users.forEach(user=>{ %>
|
<% users.filter(member=> !member.deleted || user.admin ).forEach(member => { %>
|
||||||
<div style="display:flex;justify-content:center;">
|
<div style="display:flex;justify-content:center;">
|
||||||
<div class="user-box">
|
<div class="user-box">
|
||||||
<img src="<%= user.avatar %>" class="user-box-img">
|
<img src="<%= member.avatar %>" class="user-box-img">
|
||||||
<div class="user-box-title"> <a href="<%= user.getLink() %>">
|
<div class="user-box-title"> <a href="<%= member.getLink() %>">
|
||||||
<% if (user.deleted) { %> <span style="color: var(--important);">[DELETED]</span><% } %>
|
<% if (member.deleted) { %> <span style="color: var(--important);">[DELETED]</span><% } %>
|
||||||
<%= user.name %></a></div>
|
<%= member.name %></a></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% }); %>
|
<% }); %>
|
||||||
|
|
Loading…
Reference in a new issue