Compare commits

..

2 commits

Author SHA1 Message Date
16a2504665 5.0.0 2023-05-09 13:28:55 +03:00
93bb17981f Added themes api way, and bootstrap theme,useredit 2023-05-09 13:28:45 +03:00
44 changed files with 849 additions and 254 deletions

View file

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

View file

@ -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.
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": "your_app_id"` in config.json.
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
- _id
- add support for transition around gravatar
- forum setup page++
### 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)
- forum setup page rewrite and directly open a router
### 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
- categories page is need a update, thread count in category (?)
- add ban button to user profile.?
- who liked a message for web.
- add ban button to user profile
- who liked a message
- give admin button, not is admin
- edit user ++
- rewrite main page, list new messages
## Major Version History
- V4: Caching

View file

@ -1,6 +1,6 @@
{
"def_theme": {
"name": "white",
"codename": "bootstrap_black",
"language": "en"
},
"forum_name": "akf",
@ -18,7 +18,7 @@
},
"discord_auth": "",
"default_thread_state": "OPEN",
"email_auth": false,
"default_user_state": "ACTIVE",
"email_auth": false,
"host": "https://akf-forum.glitch.me"
}

View file

@ -6,10 +6,10 @@ const
mongoose = require("mongoose"),
express = require('express'),
fs = require("fs"),
{ join } = require("path"),
app = express(),
{ mw: IP } = require('request-ip'),
{ RL } = require('./lib'),
{ themes } = require("./config.json"),
{ RL, themes } = require('./lib'),
SES = require('express-session'),
MS = require("connect-mongo"),
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 }
}) : 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, {
dataset: {
themes,
theme: req.user?.theme || def_theme,
forum_name,
description
themes, theme, forum_name, description,
getFile: file => join(__dirname, "public", "themes", file),
},
user: req.user,
...options

2
lib.js
View file

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

View file

@ -12,7 +12,7 @@ const schema = new mongoose.Schema({
about: { type: String, default: "", maxlength: limits.desp },
admin: { type: Boolean, default: false },
theme: {
name: { type: String, default: def_theme.name },
codename: { type: String, default: def_theme.codename },
language: { type: String, default: def_theme.language }
},
lastSeen: { type: Date, default: Date.now, select: false },

4
package-lock.json generated
View file

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

View file

@ -1,6 +1,6 @@
{
"name": "akf-forum",
"version": "4.22.1",
"version": "5.0.0",
"description": "A Node.js based forum software",
"main": "index.js",
"scripts": {

View file

@ -22,7 +22,7 @@ form {
}
/* MODAL */
.modal {
.forum-modal {
position: fixed;
left: 0;
top: 0;
@ -32,7 +32,7 @@ form {
}
.modal-content {
.forum-modal-content {
position: absolute;
top: 50%;
left: 50%;
@ -44,12 +44,12 @@ form {
animation: fadeIn 1s forwards;
}
.modal.no-active {
.forum-modal.no-active {
display: none;
}
.modal-close {
.forum-modal-close {
position: absolute;
top: 0;
right: 10px;
@ -69,7 +69,7 @@ form {
}
@media(max-width:760px) {
.modal-content {
.forum-modal-content {
width: 95%;
}
}

View file

@ -80,7 +80,7 @@
position: absolute;
top: 50px;
right: 0;
background-color: var(--btn-clr-1);
background-color: var(--second);
width: 100px;
text-align: center;
display: none;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

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

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

View 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>
<% } %>
<% } %>

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

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

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

View 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>
<% } %>

View file

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

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

View file

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

View file

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

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

View file

@ -1,4 +1,6 @@
/*
akf-forum black theme config file
*/
:root {
--main: #ac8fff;
--btn-clr-1: #e8e8e8;

View file

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

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,5 @@
/* akf-forum default theme config file */
:root {
--main: #4d18e6;
--btn-clr-1: #e8e8e8;

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

View file

@ -1,7 +1,7 @@
const { UserModel, BanModel } = require("../../../models");
const { Router } = require("express");
const multer = require("multer");
const { themes } = require("../../../config.json")
const { themes } = require("../../../lib")
const app = Router();
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`);
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;
if (typeof admin === "boolean" || ["false", "true"].includes(admin)) member.admin = admin;

View file

@ -18,6 +18,7 @@ app.get("/:id/avatar", async (req, res) => {
else
res.error(404, `We don't have any user with id ${req.params.id}.`);
})
app.get("/:id", async (req, res) => {
const user = req.user
const { id } = req.params;
@ -27,10 +28,23 @@ 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") })
}
else res.error(404, `We don't have any user with id ${id}.`);
});
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;

44
views/edit_user.ejs Normal file
View 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>

View file

@ -1,33 +1 @@
<div class="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>
<%- include(dataset.getFile(dataset.theme.codename +"/extra/footer")) %>

View file

@ -1,9 +1 @@
<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="/css/themes/<%= dataset.theme.name %>.css" />
<link rel="stylesheet" href="/css/common.css" />
</head>
<%- include(dataset.getFile(dataset.theme.codename +"/extra/meta")) %>

View file

@ -1,45 +1 @@
<% 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>
<%- include(dataset.getFile(dataset.theme.codename +"/extra/navbar")) %>

View file

@ -9,21 +9,16 @@
<%- include("extra/navbar") %>
<div class="content">
<% if (user) { %>
<h2 style="color: var(--main);">
<div class="box-username">Welcome, <%= user.name %>
<div class="avatar"><img src="<%=user.avatar %>"></div>
</div>
You can log out of the site here:
Main page will be rewritten soon, so stay tuned!
</h2>
<a href="/login" class="btn-outline-primary">LOGOUT</a>
<br>
<% } else { %>
<h2 style="color: var(--main);">Welcome, Guest!<br>You can press the button to register:
<a href="/register" class="btn-outline-primary">REGISTER</a>

View file

@ -1,16 +1,17 @@
<!DOCTYPE html>
<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 }) %>
<body>
<%- 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/pages.css" />
@ -142,6 +143,13 @@
if (res) location.href = `/threads/<%= thread.id %>?page=${tp-1}`;
});
</script>
<style>
.fa {
color: var(--main);
}
</style>
<% }%>
<div class="pagination">
<div class="back">

View file

@ -13,118 +13,8 @@
<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;">
<img style="width:150px;height:150px;border-radius:50%;" src="<%=member.avatar %>">
</div>
@ -133,7 +23,7 @@
<h2 class="box-value" style="align-self: center;">Admin</h2>
<% } %>
<% if (member.about?.length) { %>
<div class="box-value" id="about" style="
margin: 10px auto;
box-shadow: 0 0 5px 0 var(--second);
@ -152,6 +42,9 @@ color: var(--anti);
const about = document.getElementById("about")
about.innerHTML = converter.makeHtml(about.innerText);
</script>
<% } %>
<div class="box">
<h2 class="box-title">Name:</h2>
@ -168,12 +61,16 @@ color: var(--anti);
</div>
<% } %>
<div class="box">
<a href="/search/messages?authorID=<%= member.id %>" class="box-title">Message:</a>
<a class="box-value"><%= counts.message %></a>
<h2 class="box-title">Message:</h2>
<a class="box-value" href="/search/messages?authorID=<%= member.id %>">
<%= counts.message %>
</a>
</div>
<div class="box">
<a href="/search/threads?authorID=<%= member.id %>" class="box-title">Thread:</a>
<h2 class="box-value"><%= counts.thread %></h2>
<h2 class="box-title">Thread:</h2>
<a class="box-value" href="/search/threads?authorID=<%= member.id %>">
<%= counts.thread %>
</a>
</div>
</div>
<%- include("extra/footer") %>

View file

@ -10,15 +10,14 @@
<link rel="stylesheet" href="/css/pages.css" />
<%- include("extra/navbar") %>
<div class="users">
<% users.forEach(user=>{ %>
<% users.filter(member=> !member.deleted || user.admin ).forEach(member => { %>
<div style="display:flex;justify-content:center;">
<div class="user-box">
<img src="<%= user.avatar %>" class="user-box-img">
<div class="user-box-title"> <a href="<%= user.getLink() %>">
<% if (user.deleted) { %> <span style="color: var(--important);">[DELETED]</span><% } %>
<%= user.name %></a></div>
<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>
<% }); %>