Compare commits

..

No commits in common. "main" and "V3.4.X" have entirely different histories.
main ... V3.4.X

149 changed files with 7381 additions and 14292 deletions

View file

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

View file

@ -1,27 +0,0 @@
{
"ignorePatterns": [
"test.js"
],
"env": {
"node": true,
"commonjs": true,
"es2021": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": "latest"
},
"rules": {
"no-use-before-define": "error"
},
"overrides": [
{
"env": {
"browser": true
},
"files": [
"src/public/js/*"
]
}
]
}

9
.gitignore vendored
View file

@ -1,15 +1,8 @@
# Dependency directories
node_modules/
# Environment variables
# env
.env
# Config files
config.json
# Test files
test.js
# avatars folder
src/public/images/avatars/*
!src/public/images/avatars/default.jpg

View file

@ -1,105 +1,69 @@
# API documentation of Akf-forum
<img src="https://raw.githubusercontent.com/Akif9748/akf-forum/main/public/images/logo.jpg" align="right" width="300px" />
Akf-forum has got an API for AJAX, other clients etc.
## Authorization
You need this header for send request to API:
You need this headers for send request to API:
```json
{
"authorization": "Basic <base64 encoded username:password>"
"username": "testUser",
"password": "testPassword"
}
```
But in front end, the API will works with session.
## Default Limits:
- 3 - 25 char for username, password and category name
- 256 char for user about and desp of category
- 5 - 128 char for thread titles.
- 5 - 1024 char for messages.
You can change them in config.json.
## How to request?
### Request types:
#### `/api/me`
- GET `/api/me` to get your account.
- GET `/api/users/:id` for fetch user.
- POST `/api/users/:id/delete` for delete user.
- POST `/api/users/:id/undelete` for undelete user.
- POST `/api/users/:id/admin` for give admin permissions for a user.
- POST `/api/users/:id/edit` for edit user.
#### `/api/config`
- GET `/` to reach config file.
- PUT `/` to edit config file.
- GET `/api/threads/:id` for fetch thread.
- GET `/api/threads/:id/messages/` for fetch messages in thread.
- POST `/api/threads` for create thread.
- POST `/api/threads/:id/delete` for delete thread.
- POST `/api/threads/:id/undelete` for undelete thread.
- POST `/api/threads/:id/edit` for edit thread.
#### `/api/bans`
- GET `/` fetch all bans.
- GET `/:ip` fetch a ban.
- DELETE `/:ip` for unban an IP adress.
- POST `/?reason=flood` for ban an IP adress.
#### `/api/categories`
- GET `/` fetch all categories.
- GET `/:id` fetch a category
- PATCH `/:id` for update a category.
- DELETE `/:id` for delete a category.
- POST `/` for create a category.
#### `/api/messages`
- GET `/:id` for fetch message.
- DELETE `/:id` for delete message.
- PATCH `/:id` for edit message.
- POST `/:id/react/:type` for react to a message.
- POST `/` for create message.
#### `/api/search` use `?limit=&skip=` for skip and limit
- GET `/users?q=query` find users.
- GET `/threads?q=query&authorID=not_required` find threads.
- GET `/messages?q=query&authorID=not_required` find messages.
#### `/api/threads`
- GET `/:id` for fetch thread.
- DELETE `/:id` for delete thread.
- PATCH `/:id` for edit thread.
- GET `/:id/messages?skip=0&limit=10` for fetch messages in thread.
- POST `/` for create thread.
#### `/api/users`
- GET `/:id` for fetch user.
- DELETE `/:id` for delete user.
- PATCH `/:id` for edit user.
- PUT `/:id` for add profile photo to user.
- POST `/:id/ban` for ban all ips of user.
- GET `/api/messages/:id` for fetch message.
- POST `/api/messages` for create message.
- POST `/api/messages/:id/delete` for delete message.
- POST `/api/messages/:id/undelete` for undelete message.
- POST `/api/messages/:id/react/:type` for react to a message.
- POST `/api/messages/:id/edit` for edit message.
### Example request:
GET ```/api/messages/0```
#### Example API Output:
```js
```json
{
"react": {
"like": [],
"dislike": ["0"]
},
"_id": "6325c216faa938c4cfc43075",
"_id": "63067429bc01da866fad508b",
"threadID": "0",
"author": {
"_id": "632e028ca4ba362ebbb75a43",
"id": "0",
"name": "Akif9748",
"avatar": "/images/avatars/0.jpg",
"avatar": "https://cdn.discordapp.com/avatars/539506680140922890/abd74d10aac094fc8a5ad5c86f29fdb9.png?size=1024",
"time": "2022-08-24T18:54:55.666Z",
"deleted": false,
"edited": true,
"about": "# Owner",
"admin": true,
"theme": "black",
"hideLastSeen": false,
"time": "2022-09-23T19:01:32.610Z",
"admin": false,
"_id": "630673ffbc01da866fad507b",
"__v": 0
},
"content": "deneme",
"deleted": false,
"edited": false,
"time": "2022-08-24T18:55:37.744Z",
"id": "0",
"__v": 0,
"discordID": "539506680140922890"
"react": {
"like": [0],
"dislike":[]
},
"threadID": "0",
"content": "This is a thread opened via API, yes",
"deleted": false,
"edited": true,
"time": "2022-09-17T12:48:22.378Z",
"id": "0",
"__v": 4,
"oldContents": []
"authorID": "0"
}
```

146
README.md
View file

@ -1,37 +1,19 @@
# akf-forum
<img src="https://raw.githubusercontent.com/Akif9748/akf-forum/main/public/images/logo.jpg" align="right" width="300px" />
A Node.js based forum software.
## Installation
- Clone or download this repo.
- Run `npm i` to install **dependencies**.
- Enter your database credentials in `.env`.
- Run `npm start` for run it.
- Go `/setup` page for setup your forum.
### Extra (If you are not use `setup` page)
Run `node util/reset` to **reset the database** for duplicate key errors, and run `node util/admin` for give admin perms to first member.
Edit `config.json` for default theme for users, forum name, meta description, character limits, discord auth enabler, global ratelimit etc.
### How to install theme:
- Copy your theme to `src/themes` folder.
Additional note for themes: If a theme has not got any .ejs file, it will use default theme's .ejs files. default theme is in themes folder, named as `common`.
### DISCORD AUTH:
`"discord_auth": true` in config.json.
Add your app secret and app id to `.env` as `DISCORD_SECRET` and `DISCORD_ID`.
Create a redirect url in discord developer portal:
`https://forum_url.com/auth/discord`
### EMAIL AUTH:
You can configure it. Just edit `config.json` and `.env` files.
`"email_auth": true, "default_user_state": "APPROVAL"` in config.json.
Add your email credentials to `.env` as `EMAIL_USER` and `EMAIL_PASS`.
Add your email domain to `.env` as `EMAIL_SERVICE`.
### Extra
Run `node util/reset` to **reset the database**, and run `node util/admin` for give admin perms to first member.
Edit `config.json` for default themes of users...
## API
Akf-forum has got an API for AJAX (fetch), other clients etc. And, you can learn about API in `APIDOCS.md`.
**And you can use [offical API wrapper](https://github.com/Akif9748/akf-forum-api).**
Akf-forum has got an API for AJAX, other clients etc. And, you can learn about API in `util/APIDOCS.md`.
## Credits
* [Akif9748](https://github.com/Akif9748) - Project mainteiner, main developer, made **old** frontend
@ -39,65 +21,81 @@ Akf-forum has got an API for AJAX (fetch), other clients etc. And, you can learn
* [Camroku](https://github.com/Camroku) - Made **old** stylesheets
## Screenshot
### Old frontend
![akf-forum](https://user-images.githubusercontent.com/70021050/160255959-ef216cba-1348-4d4b-9347-fe67e21348e7.png)
### New frontend
![new-akf-forum](https://user-images.githubusercontent.com/70021050/186941146-f9a8fbf8-9b2b-4028-afc8-81cff559d9fb.png)
### Thread Page w/Bootstrap theme
![image](https://github.com/Akif9748/akf-forum/assets/70021050/1ad4ad8e-d000-46a6-834e-7d76cdddda60)
## TO-DO list
### Backend:
#### Feature:
- Profile Message or DM
- Upload other photos, model for it
- Edit & download template
- Banner
- Add @me support for ids, <%= member.id %>
- Roles & Permissions
```
role: "moderator",
permissions: ["see_deleted_message"]
```
#### Fixes:
- Admin deleting other admins.
- theme.js, change theme js code to a file.
- same email discord???? direct err
- IMPORTANT: add user/member id to file so scripts can access
#### ETC:
- Rewrite apidocs
- Add a feature list to README.md
## Roadmap
### TO-DO:
- If thread deleted, not show its messages in API.
- Profile photos will store in database
- replacer function global
- author name of thread
- page for threads - users []
- API, ?fast=
- extra ratelimits
- better edits
### Frontend
#### Features:
- change category name
- Add approval threads page.
- add support for switch around gravatar and upload photo
- old contents / titles add to forum interface
- who liked a message
#### User
| To do | Is done? | Priority |
| ----- | -------- | -------- |
| Login via redirect query | 🟢 | HIGH |
| Register | 🟢 | HIGH |
| Logout | 🟢 | HIGH |
| Admin | 🟢 | HIGH |
| Message count | 🟢 | MEDIUM |
| Delete user | 🟢 | HIGH |
| Undelete | 🟢 | MEDIUM |
| About me | 🟢 | LOW |
| Edit user | 🟢 | HIGH |
| IP ban | 🟢 | MEDIUM |
| Profile Message | 🔴 | MEDIUM |
#### Fixes:
- BETTER SETUP PAGE: use setup everytime
- add threads, messages etc. to "extra" folder
- add category to thread list page
- working reset button
- text alling center body
- thread.js unfuction only listener
- show error on modal
- send delete, ban to user settings (edit user) menu and fix edit user menu
#### Messages
| To do | Is done? | Priority |
| ----- | -------- | -------- |
| Ratelimit | 🟢 | HIGH |
| Send | 🟢 | HIGH |
| Delete | 🟢 | HIGH |
| Regex for scripts | 🟢 | HIGH |
| Undelete | 🟢 | MEDIUM |
| React | 🟢 | MEDIUM |
| Edit | 🟢 | MEDIUM |
## Special Thanks:
https://github.com/akashgiricse/Online-Forum for bootstrap theme.
#### Threads
| To do | Is done? | Priority |
| ----- | -------- | -------- |
| Ratelimit | 🟢 | HIGH |
| Create | 🟢 | HIGH |
| Delete | 🟢 | HIGH |
| Undelete | 🟢 | MEDIUM |
| Edit | 🟢 | MEDIUM |
@Tokmak for old frontend.
https://fengyuanchen.github.io/cropperjs/examples/crop-a-round-image.html for avatar upload panel.
https://github.com/mdbootstrap/bootstrap-profiles for profile page of bootstrap theme.
### API
| To do | Is done?
| ----- | --------
| RATELIMITS | 🟢
| Get message**s** | 🟢
| Create message & thread & user | 🟢
| Get message & thread & user | 🟢
| Delete message & thread & user | 🟢
| Undelete message & thread & user | 🟢
| Edit message & thread & user | 🟢
### Other
| To do | Is done? | Priority |
| ----- | -------- | -------- |
| from form to AJAX | 🟢 | HIGH |
| auto-scroll | 🟢 | LOW |
| Page support, support message limit correct | 🟢 | MEDIUM |
| Multi-theme support, black theme | 🟡 | LOW |
| Search | 🔴 | MEDIUM |
| Locales | 🔴 | LOW |
| Footer | 🔴 | LOW |
## Major Version History
- V5: Enchanted Themes
- V4: Caching
- V3: New Theme
- V2: Backend fix, mongoose is fixed. Really big fix.
- V1: Mongoose added.

3
config.json Normal file
View file

@ -0,0 +1,3 @@
{
"def_theme": "default"
}

View file

@ -1,24 +0,0 @@
{
"def_theme": {
"codename": "bootstrap_black",
"language": "en"
},
"forum_name": "akf-forum",
"description": "Akf-forum offical site!",
"limits": {
"title": 128,
"message": 1024,
"names": 25,
"desp": 256
},
"global_ratelimit": {
"enabled": true,
"max": 30,
"windowMs": 60000
},
"discord_auth": false,
"default_thread_state": "OPEN",
"default_user_state": "ACTIVE",
"email_auth": false,
"host": "https://akf-forum.glitch.me"
}

44
index.js Normal file
View file

@ -0,0 +1,44 @@
const { def_theme } = require("./config.json"),
ipBlock = require('express-ip-block'),
session = require('express-session'),
{ UserModel, BanModel } = require("./models"),
bodyParser = require('body-parser'),
port = process.env.PORT || 3000,
mongoose = require("mongoose"),
express = require('express'),
fs = require("fs"),
app = express();
app.ips = [];
require("dotenv").config();
mongoose.connect(process.env.MONGO_DB_URL,
async () => console.log("Connected to mongoDB with", app.ips = await BanModel.find({}).select("ip"), "banned IPs"));
app.set("view engine", "ejs");
app.use(session({ secret: 'secret', resave: true, saveUninitialized: true }),
bodyParser.urlencoded({ extended: true }),
express.static("public"), express.json(), ipBlock(app.ips),
async (req, res, next) => {
req.user = await UserModel.get(req.session.userid);
res.reply = (page, options = {}, status = 200) => res.status(status)
.render(page, { user: req.user, theme: req.user?.theme || def_theme, ...options });
res.error = (type, error) => res.reply("error", { type, error }, type);
if (req.user?.deleted) {
req.session.destroy();
return res.error(403, "Your account has been deleted.");
}
next();
}
);
for (const file of fs.readdirSync("./routes"))
app.use("/" + file.replace(".js", ""), require(`./routes/${file}`));
app.all("*", (req, res) => res.error(404, "We have not got this page."));
app.listen(port, () => console.log("akf-forum on port:", port));

7
models/Ban.js Normal file
View file

@ -0,0 +1,7 @@
const mongoose = require("mongoose")
const schema = new mongoose.Schema({
ip: { type: String, unique: true }
});
module.exports = mongoose.model('ban', schema);

36
models/Message.js Normal file
View file

@ -0,0 +1,36 @@
const mongoose = require("mongoose")
const UserModel = require("./User");
const schema = new mongoose.Schema({
id: { type: String, unique: true },
threadID: String,
author: UserModel.schema, // user-model
content: String,
time: { type: Date, default: Date.now },
deleted: { type: Boolean, default: false },
edited: { type: Boolean, default: false },
react: {
like: [Number],
dislike: [Number]
}
})
schema.virtual('authorID').get(function () { return this.author?.id; });
schema.methods.takeId = async function () {
this.id = String(await model.count() || 0);
return this;
}
schema.methods.getLink = function (id = this.id) {
return "/messages/" + id;
}
const model = mongoose.model('message', schema);
model.get = id => model.findOne({ id });
module.exports = model;

9
models/Secret.js Normal file
View file

@ -0,0 +1,9 @@
const mongoose = require("mongoose")
const schema = new mongoose.Schema({
username: { type: String, unique: true },
password: String,
id: { type: String, unique: true }
});
module.exports = mongoose.model('secret', schema);

44
models/Thread.js Normal file
View file

@ -0,0 +1,44 @@
const mongoose = require("mongoose");
const UserModel = require("./User");
const MessageModel = require("./Message");
const schema = new mongoose.Schema({
id: { type: String, unique: true },
author: UserModel.schema,
title: String,
time: { type: Date, default: Date.now },
deleted: { type: Boolean, default: false },
edited: { type: Boolean, default: false },
messages: [String],
views: { type: Number, default: 0 }
});
schema.virtual('authorID').get(function () { return this.author?.id; });
schema.methods.messageCount = async function (admin = false) {
const query = { threadID: this.id };
if (!admin) query.deleted = false;
return await MessageModel.count(query) || 0;
};
schema.methods.push = function (messageID) {
this.messages.push(messageID);
return this;
}
schema.methods.takeId = async function () {
this.id = await model.count() || 0;
return this;
}
schema.methods.getLink = function (id = this.id) {
return "/threads/" + id;
}
const model = mongoose.model('thread', schema);
model.get = id => model.findOne({ id });
module.exports = model;

31
models/User.js Normal file
View file

@ -0,0 +1,31 @@
const mongoose = require("mongoose")
const { def_theme } = require("../config.json");
const schema = new mongoose.Schema({
id: { type: String },
name: String,
avatar: { type: String, default: "/images/guest.png" },
time: { type: Date, default: Date.now },
deleted: { type: Boolean, default: false },
edited: { type: Boolean, default: false },
about: { type: String, default: "" },
admin: { type: Boolean, default: false },
theme: { type: String, default: def_theme }
});
schema.methods.takeId = async function () {
this.id = String(await model.count() || 0);
return this;
}
schema.methods.getLink = function (id = this.id) {
return "/users/" + id;
}
const model = mongoose.model('user', schema);
model.get = id => model.findOne({ id });
module.exports = model;

7
models/index.js Normal file
View file

@ -0,0 +1,7 @@
const UserModel = require("./User"),
MessageModel = require("./Message"),
ThreadModel = require("./Thread"),
SecretModel = require("./Secret"),
BanModel = require("./Ban");
module.exports = { UserModel, MessageModel, ThreadModel, SecretModel, BanModel };

8213
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,10 @@
{
"name": "akf-forum",
"version": "5.7.1",
"version": "3.4.1",
"description": "A Node.js based forum software",
"main": "src/index.js",
"main": "index.js",
"scripts": {
"start": "node .",
"reset": "node util/reset",
"admin": "node util/admin",
"test": "echo \"Error: no test specified\" && exit 1"
"start": "node ."
},
"repository": {
"type": "git",
@ -23,23 +20,20 @@
"url": "https://github.com/Akif9748/akf-forum/issues"
},
"engines": {
"node": ">=18 >=17.5 >=16.15"
"node": ">=16"
},
"homepage": "https://akf-forum.glitch.me/",
"dependencies": {
"bcrypt": "^5.1.1",
"connect-mongo": "^5.1.0",
"dotenv": "^16.4.2",
"ejs": "^3.1.9",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"express-session": "^1.18.0",
"mongoose": "^8.1.1",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.9",
"request-ip": "^3.3.0"
},
"devDependencies": {
"eslint": "^8.56.0"
"bcrypt": "^5.0.1",
"body-parser": "^1.19.2",
"dotenv": "^16.0.1",
"ejs": "^3.1.6",
"express": "^4.17.3",
"express-ip-block": "^0.1.2",
"express-rate-limit": "^6.5.1",
"express-session": "^1.17.2",
"i": "^0.3.7",
"mongoose": "^6.5.1",
"npm": "^8.18.0"
}
}

2114
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,23 @@
.title {
color: #4d18e6;
font-weight: 700;
}
form {
display: flex;
flex-direction: column;
align-items: center;
max-width: 1000px;
margin: 0 auto;
}
.input {
padding: 8px 10px;
font-family: inherit;
display: block;
font-weight: 600;
color: #4c4c4c;
width: 500px;
margin-bottom: 10px;
border: 2px solid #d9d9d9;
}

24
public/css/login.css Normal file
View file

@ -0,0 +1,24 @@
.title {
color: #4d18e6;
font-weight: 700;
}
form {
display: flex;
align-items: center;
flex-direction: column;
max-width: 1000px;
margin: 0 auto;
}
.input {
padding: 8px 10px;
font-family: inherit;
display: block;
font-weight: 600;
color: #4c4c4c;
width: 500px;
margin-bottom: 10px;
border: 2px solid #d9d9d9;
}

166
public/css/navbar.css Normal file
View file

@ -0,0 +1,166 @@
@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');
* {
box-sizing: border-box;
}
body {
margin: 0;
}
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: #4d18e6;
font-weight: 900;
}
.logo>span {
color: #606060;
}
.buttons {
padding: 0;
display: flex;
align-items: center;
}
.btn-primary {
color: #e8e8e8;
background-color: #4d18e6;
padding: 10px 20px 10px 20px;
border-radius: 4px;
font-weight: 700;
margin: 10px;
cursor: pointer;
border: 2px solid #4d18e6;
}
.btn-outline-primary {
color: #4d18e6;
padding: 10px 20px 10px 20px;
border-radius: 4px;
font-weight: 700;
margin: 10px;
cursor: pointer;
border: 2px solid #e2e2e2;
}
.menu {
width: 100%;
padding: 20px;
display: flex;
justify-content: space-around;
}
.btn-primary:hover {
color: #4d18e6;
background-color: rgba(0, 0, 0, 0);
border: 2px solid #4d18e6;
}
.menu-item {
padding: 10px;
font-weight: 700;
background-color: #606060;
color: #ffffff;
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: #4d18e6;
}
body {
font-family: Poppins;
}
.btn-outline-primary:hover {
border: 2px solid #4d18e6;
}
.menu-item:hover {
background-color: #4d18e6;
}
.admin-bar {
font-size: 15px;
color: #ffffff;
background: #4d18e6;
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;
font-size: 18px;
font-weight: 700;
color: #4d18e6;
}
@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;
}
}

140
public/css/thread.css Normal file
View file

@ -0,0 +1,140 @@
.title {
color: #414141;
font-weight: 700;
font-size: 36px;
}
.date {
color: gray;
}
.message {
max-width: 800px;
box-shadow: 0 0 5px 0 #c3c3c3;
margin: 10px auto;
padding: 20px;
display: flex;
gap: 10px;
position: relative;
}
.message .left {
text-align: center;
border-right: 2px solid #d9d9d9;
}
.message .left img {
width: 100px;
height: 100px;
border-radius: 50%;
margin-right: 5px;
}
.message .left .username {
color: #555;
}
.content {
width: 70%;
}
.reactions {
position: absolute;
right: 20px;
bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.reactions div {
display: flex;
align-items: center;
gap: 5px;
padding: 4px;
color: #747474;
cursor: pointer;
transition: color 0.3s ease;
}
.reactions div:hover {
color: #151515;
}
.reactions div i {
font-size: 22px;
}
.pagination {
box-shadow: 0 0 5px 0 #c3c3c3;
margin: 10px auto;
padding: 8px;
display: flex;
justify-content: space-between;
align-items: center;
max-width: 400px;
gap: 10px;
position: relative;
}
.pagination .back,
.pagination .after {
color: #747474;
font-size: 26px;
cursor: pointer;
}
.pagination .numbers {
display: flex;
align-items: center;
gap: 5px;
}
.pagination .number {
color: #747474;
font-size: 22px;
border: 0 0 5px #747474;
padding: 8px;
border-radius: 2px;
font-weight: 600;
cursor: pointer;
margin: 8px;
}
.pagination .number.active {
color: #4d18e6;
font-weight: 700;
}
.dots {
position: absolute;
right: 20px;
font-size: 22px;
top: 10px;
color: #747474;
cursor: pointer;
}
.dots-menu {
position: absolute;
top: 50px;
right: 0;
background-color: #e6e6e6;
width: 100px;
text-align: center;
display: none;
}
.dots-menu.active {
display: block;
}
.dots-menu a {
display: block;
margin: 8px;
cursor: pointer;
}

36
public/css/threads.css Normal file
View file

@ -0,0 +1,36 @@
.threads {
width: 100%;
padding: 20px;
}
.threads-box {
width: 100%;
padding: 8px;
box-shadow: 0 0 5px 0 #cbcbcb;
display: flex;
justify-content: space-between;
margin: 0px 0px 8px 0px;
cursor: pointer;
align-items: center;
}
.threads-box:hover {
background-color: #e2e2e2;
}
.thread-box-title {
padding: 10px;
font-size: 18px;
font-weight: 700;
}
.thread-box-title>span {
color: #ff0000;
}
@media (max-width: 480px) {
.threads-box {
flex-direction: column;
}
}

31
public/css/user.css Normal file
View file

@ -0,0 +1,31 @@
.title {
color: #4d18e6;
font-weight: 700;
}
.content {
display: flex;
flex-direction: column;
box-shadow: 0 0 5px 0 #bebebe;
max-width: 900px;
margin: 0 auto;
padding: 10px;
}
.box {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 auto;
max-width: 800px;
width: 100%;
}
.box-title{
color: #4d18e6;
font-weight: 300;
}
.box-value {
color: #606060;
font-weight: 300;
}

43
public/css/users.css Normal file
View file

@ -0,0 +1,43 @@
.users {
width: 100%;
padding: 20px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 10px;
}
.user-box {
width: 100%;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 0 5px 0 #beb9b9;
max-width:500px;
}
.user-box-title {
padding: 10px;
margin: 8px;
font-weight: 500;
}
.user-box-title>span {
color: #ff0000;
}
.user-box-img {
width: 80px;
height: 80px;
margin: auto;
}
@media (max-width: 992px) {
.users {
display: block;
}
.user-box {
margin-bottom: 10px;
}
}

BIN
public/images/favicon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

BIN
public/images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

View file

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
public/images/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

53
public/js/thread.js Normal file
View file

@ -0,0 +1,53 @@
import request from "./request.js";
window.edit_t = async function (id) {
const title = prompt("Enter new title!");
const res = await request(`/api/threads/${id}/edit`, "POST", { title });
if (res.error) return;
alert(`Thread updated`);
document.getElementById("title").innerHTML = title;
}
window.thread = async function (id, un = "") {
const res = await request(`/api/threads/${id}/${un}delete`);
if (res.error) return;
alert(`Thread ${un}deleted`);
location.reload();
}
window.edit_message = async function (id) {
const content = prompt("Enter new content!");
const res = await request(`/api/messages/${id}/edit`, "POST", { content });
if (res.error) return;
alert(`Message updated`);
document.getElementById("message-" + id).querySelector(".content").innerHTML = content;
}
window.undelete_message = async function (id) {
const response = await request(`/api/messages/${id}/undelete`);
if (response.deleted) return;
document.getElementById("deleted-" + id).remove();
document.getElementById("dot-" + id).innerHTML = `
<a onclick="delete_message('${id}');">DELETE</a>
<a onclick="edit_message('${id}');">EDIT</a>`
}
window.delete_message = async function (id) {
const response = await request(`/api/messages/${id}/delete`);
if (response.deleted) {
alert("Message deleted");
document.getElementById("dots-" + id).innerHTML = `
<i class='bx bx-trash bx-sm' id="deleted-${id}" style="color: RED;"></i>
`+ document.getElementById("dots-" + id).innerHTML;
document.getElementById("dot-" + id).innerHTML = `<a onclick="undelete_message('${id}');">UNDELETE</a>`;
}
}
window.react = async function (id, type) {
const res = await request(`/api/messages/${id}/react/${type}`)
document.getElementById(`like-${id}`).innerHTML = res.react.like.length;
document.getElementById(`dislike-${id}`).innerHTML = res.react.dislike.length;
}

19
routes/.js Normal file
View file

@ -0,0 +1,19 @@
const { UserModel, ThreadModel, MessageModel } = require("../models")
const { Router } = require("express");
const app = Router();
app.get("/", async (req, res) => {
const
mem = process.memoryUsage().heapUsed / Math.pow(2, 20),
users = await UserModel.count({deleted:false}),
threads = await ThreadModel.count({deleted:false}),
messages = await MessageModel.count({deleted:false}),
user = req.user;
res.reply("index", { mem, users, threads, messages })
})
module.exports = app;

9
routes/admin.js Normal file
View file

@ -0,0 +1,9 @@
const { Router } = require("express")
const app = Router();
app.get("/", async (req, res) => {
if (!req.user?.admin) return res.error(403, "You have not got permissions for view to this page.");
res.reply("admin")
});
module.exports = app;

42
routes/api/index.js Normal file
View file

@ -0,0 +1,42 @@
const { Router, request, response } = require("express")
const app = Router();
const bcrypt = require("bcrypt");
const { SecretModel, UserModel } = require("../../models")
/**
* Auth checker
* @param {request} req
* @param {response} res
*/
app.use(async (req, res, next) => {
res.error = (status, error) => res.status(status).json({ error });
res.complate = result => res.status(200).json(result);
if (req.user) return next();
const { username = null, password = null } = req.headers;
if (!username || !password)
return res.error(401, "Authorise headers are missing")
const user = await SecretModel.findOne({ username });
if (!user)
return res.error(401, `We don't have any thread with name ${username}.`)
if (!await bcrypt.compare(password, user.password)) return res.error(401, 'Incorrect Password!');
req.user = await UserModel.findOne({ name: req.headers.username });
next();
});
/* will add for loop */
app.use("/messages", require("./routes/messages"))
app.use("/users", require("./routes/users"))
app.use("/threads", require("./routes/threads"))
app.all("*", (req, res) => res.error(400, "Bad request"));
module.exports = app;

View file

@ -0,0 +1,121 @@
const { MessageModel, ThreadModel } = require("../../../models");
const rateLimit = require('express-rate-limit')
const { Router } = require("express")
const app = Router();
app.get("/:id", async (req, res) => {
const message = await MessageModel.get(req.params.id);
if (!message || (message.deleted && req.user && !req.user.admin)) return res.error(404, `We don't have any message with id ${req.params.id}.`);
res.complate(message.toObject({ virtuals: true }));
})
app.post("/:id/edit", async (req, res) => {
const message = await MessageModel.get(req.params.id);
if (!message || (message.deleted && req.user && !req.user.admin)) return res.error(404, `We don't have any message with id ${req.params.id}.`);
if (req.user.id !== message.authorID && !req.user.admin) return res.error(403, "You have not got permission for this.");
const { content = null } = req.body;
if (!content) return res.error(400, "Missing message content in request body.");
message.content = content;
message.edited=true;
await message.save();
res.complate(message.toObject({ virtuals: true }));
})
app.post("/", rateLimit({
windowMs: 60_000, max: 1, standardHeaders: true, legacyHeaders: false,
handler: (request, response, next, options) =>
!request.user.admin ?
response.error(options.statusCode, "You are begin ratelimited")
: next()
}), async (req, res) => {
const { threadID = null, content = null } = req.body;
if (!content) return res.error(400, "Missing message content in request body.");
const thread = await ThreadModel.get(threadID);
if (!thread) return res.error(404, `We don't have any thread with id ${threadID}.`);
const message = await new MessageModel({ content, author: req.user, threadID: thread.id }).takeId();
await message.save();
await thread.push(message.id).save();
res.complate(message.toObject({ virtuals: true }));
})
app.post("/:id/react/:type", async (req, res) => {
const message = await MessageModel.get(req.params.id);
if (!message) return error(res, 404, `We don't have any message with id ${req.params.id}.`);
if (req.params.type == "like") {
if (message.react.like.includes(req.user.id))
message.react.like.pull(req.user.id);
else {
message.react.like.push(req.user.id);
message.react.dislike.pull(req.user.id);
}
} else if (req.params.type == "dislike") {
if (message.react.dislike.includes(req.user.id))
message.react.dislike.pull(req.user.id);
else {
message.react.dislike.push(req.user.id);
message.react.like.pull(req.user.id);
}
} else {
return res.error(400, `We don't have any react type with name ${req.params.type}.`);
}
await message.save();
res.complate(message.toObject({ virtuals: true }));
});
app.post("/:id/delete", async (req, res) => {
const message = await MessageModel.get(req.params.id);
if (!message || (message.deleted && req.user && !req.user.admin))
return res.error(404, `We don't have any message with id ${req.params.id}.`);
const user = req.user;
if (user.id != message.authorID && !user.admin)
return res.error(403, "You have not got permission for this.");
message.deleted = true;
await message.save();
res.complate(message.toObject({ virtuals: true }));
})
app.post("/:id/undelete", async (req, res) => {
if (!req.user.admin) return res.error(403, "You have not got permission for this.");
const message = await MessageModel.get(req.params.id);
if (!message) return res.error(404, `We don't have any message with id ${req.params.id}.`);
if (!message.deleted) return res.error(404, "This message is not deleted, first, delete it.");
message.deleted = false;
await message.save();
res.complate(message.toObject({ virtuals: true }));
})
module.exports = app;

View file

@ -0,0 +1,101 @@
const { MessageModel, ThreadModel } = require("../../../models");
const { Router } = require("express")
const app = Router();
app.get("/:id", async (req, res) => {
const { id } = req.params;
const thread = await ThreadModel.get(id);
if (thread && (req.user?.admin || !thread.deleted))
res.complate(thread.toObject({ virtuals: true }));
else
return res.error(404, `We don't have any thread with id ${id}.`);
});
app.get("/:id/messages/", async (req, res) => {
const { id } = req.params;
const limit = Number(req.query.limit);
const skip = Number(req.query.skip);
const query = { threadID: id };
if (!req.user.admin) query.deleted = false;
const options = { sort: { time: -1 } };
if (limit) options.limit = limit;
if (skip) options.skip = skip;
const messages = await MessageModel.find(query, null, options)
if (!messages.length) return res.error(404, "We don't have any messages in this with your query thread.");
res.complate(messages.map(x => x.toObject({ virtuals: true })));
})
app.post("/", async (req, res) => {
const { title = null, content = null } = req.body;
if (!content || !title) return res.error(400, "Missing content/title in request body.");
const user = req.user;
const thread = await new ThreadModel({ title, author: user }).takeId()
const message = await new MessageModel({ content, author: user, threadID: thread.id }).takeId()
await thread.push(message.id).save();
await message.save();
res.complate(thread.toObject({ virtuals: true }));
});
app.post("/:id/edit", async (req, res) => {
const thread = await ThreadModel.get(req.params.id);
if (!thread || (thread.deleted && req.user && !req.user.admin)) return res.error(404, `We don't have any message with id ${req.params.id}.`);
if (req.user.id !== thread.authorID && !req.user.admin) return res.error(403, "You have not got permission for this.");
const { title = null } = req.body;
if (!title) return res.error(400, "Missing thread title in request body.");
thread.title = title;
await thread.save();
res.complate(thread.toObject({ virtuals: true }));
})
app.post("/:id/delete", async (req, res) => {
const thread = await ThreadModel.get(req.params.id);
if (!thread || thread.deleted) return res.error(404, `We don't have any thread with id ${req.params.id}.`);
const user = req.user;
if (user.id != thread.authorID && !user.admin)
return res.error(403, "You have not got permission for this.");
thread.deleted = true;
await thread.save();
res.complate(thread.toObject({ virtuals: true }));
})
app.post("/:id/undelete", async (req, res) => {
if (!req.user.admin) return res.error(403, "You have not got permission for this.");
const thread = await ThreadModel.get(req.params.id);
if (!thread ) return res.error(404, `We don't have any thread with id ${req.params.id}.`);
if (!thread.deleted) return res.error(404, "This thread is not deleted, first, delete it.");
thread.deleted = false;
thread.edited=true;
await thread.save();
res.complate(thread.toObject({ virtuals: true }));
})
module.exports = app;

View file

@ -0,0 +1,90 @@
const { UserModel } = require("../../../models");
const { Router } = require("express")
const app = Router();
app.get("/:id", async (req, res) => {
const { id = null } = req.params;
const member = await UserModel.get(id);
if (!member || (member.deleted && !req.user.admin)) return res.error(404, `We don't have any user with id ${id}.`);
res.complate(member);
});
app.post("/:id/delete/", async (req, res) => {
const user = req.user;
if (!user.admin)
return res.error(403, "You have not got permission for this.");
const { id = null } = req.params;
const member = await UserModel.get(id);
if (!member || member.deleted) return res.error(404, `We don't have any user with id ${id}.`);
member.deleted = true;
await member.save();
res.complate(member);
});
app.post("/:id/undelete/", async (req, res) => {
if (!req.user.admin) return res.error(403, "You have not got permission for this.");
const member = await UserModel.get(req.params.id);
if (!member) return res.error(404, `We don't have any user with id ${req.params.id}.`);
if (!member.deleted) return res.error(404, "This user is not deleted, first, delete it.");
member.deleted = false;
await member.save();
res.complate(member.toObject({ virtuals: true }));
})
app.post("/:id/edit", async (req, res) => {
const member = await UserModel.get(req.params.id);
if (!member || (member.deleted && !req.user.admin)) return res.error(404, `We don't have any message with id ${req.params.id}.`);
if (req.user.id !== member.id && !req.user.admin) return res.error(403, "You have not got permission for this.");
const { avatar, name, about } = req.body;
if (!avatar && !name) return res.error(400, "Missing member informations in request body.");
if (avatar && /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g.test(avatar))
member.avatar = avatar;
if (name) member.name = name;
if (about) member.about = about;
member.edited = true;
await member.save();
res.complate(member.toObject({ virtuals: true }));
})
app.post("/:id/admin/", async (req, res) => {
const user = req.user;
if (!user.admin) return res.error(403, "You have not got permission for this.");
const user2 = await UserModel.get(req.params.id);
if (!user2)
return res.error(404, `We don't have any user with id ${id}.`);
user2.admin = true;
await user2.save()
res.complate(user2);
});
module.exports = app;

36
routes/login.js Normal file
View file

@ -0,0 +1,36 @@
const { UserModel, SecretModel } = require("../models");
const { Router } = require("express");
const app = Router();
const bcrypt = require("bcrypt");
app.get("/", (req, res) => res.reply("login", { redirect: req.query.redirect, user: null }));
app.post("/", async (req, res) => {
req.session.userid = null;
const { username = null, password = null } = req.body;
if (username && password) {
const user = await SecretModel.findOne({ username });
if (user) {
if (!await bcrypt.compare(password, user.password)) return res.error(403, 'Incorrect Password!')
const member = await UserModel.findOne({ name: username });
if (!member || member.deleted) return res.error(403, 'Incorrect Username and/or Password!')
req.session.userid = user.id;
res.redirect(req.query.redirect || '/');
} else
res.error(403, 'Incorrect Username and/or Password!')
} else
res.error(400, "You forgot entering some values")
})
module.exports = app;

View file

@ -7,9 +7,9 @@ const app = Router();
app.get("/:id", async (req, res) => {
const message = await MessageModel.get(req.params.id);
if (!message || (message.deleted && !req.user?.admin)) return res.error( 404,
if (!message || (message.deleted && req.user && !req.user.admin)) return res.error( 404,
`We don't have any message with id ${req.params.id}.`);
res.redirect(`/threads/${message.threadID}#message-${message.id}`);
res.redirect(`/threads/${message.threadID}?scroll=${message.id}`);
});

45
routes/register.js Normal file
View file

@ -0,0 +1,45 @@
const { UserModel, SecretModel } = require("../models");
const { Router } = require("express")
const bcrypt = require("bcrypt");
const rateLimit = require('express-rate-limit')
const app = Router();
app.get("/", rateLimit({
windowMs: 24 * 60 * 60_000, max: 1, standardHeaders: true, legacyHeaders: false,
handler: (_r, response, _n, options) => response.error(options.statusCode, "You are begin ratelimited")
}), (req, res) => res.reply("register", { user: null }));
app.post("/", async (req, res) => {
req.session.userid = null;
let { username = null, password: body_pass = null, avatar, about } = req.body;
if (!username || !body_pass) return res.error(res, 400, "You forgot entering some values");
const user = await SecretModel.findOne({ username });
if (user) return res.error(res, 400, `We have got an user named ${username}!`)
const user2 = new UserModel({ name: req.body.username })
if (avatar && /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g.test(avatar)) user2.avatar = avatar;
if (about) user2.about = about;
await user2.takeId()
await user2.save();
const salt = await bcrypt.genSalt(10);
const password = await bcrypt.hash(body_pass, salt);
await SecretModel.create({ username, password, id: user2.id })
req.session.userid = user2.id;
res.redirect('/');
})
module.exports = app;

49
routes/threads.js Normal file
View file

@ -0,0 +1,49 @@
const { Router } = require("express");
const app = Router();
const { ThreadModel, MessageModel } = require("../models")
app.get("/", async (req, res) => {
const threads = await ThreadModel.find(req.user?.admin ? {} : { deleted: false })//.limit(10);
return res.reply("threads", { threads });
});
app.get("/create*", (req, res) => res.reply("create_thread"));
app.get("/:id/", async (req, res) => {
const { user, params: { id } } = req
let page = Number(req.query.page || 0);
const thread = await ThreadModel.get(id)
thread.count = await thread.messageCount(user?.admin);
thread.pages = Math.ceil(thread.count / 10);
if (thread && (user?.admin || !thread.deleted)) {
thread.views++;
const query = { threadID: id };
if (!user || !user.admin) query.deleted = false;
const messages = await MessageModel.find(query).sort({ time: 1 }).limit(10).skip(page * 10)
.then(messages => messages.map(message => {
message.content = message.content.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;").replaceAll(">", "&gt;")
.replaceAll("\"", "&quot;").replaceAll("'", "&#39;")
.replaceAll("\n", "<br>");
return message.toObject({ virtuals: true });
}))
res.reply("thread", { page, thread, messages, scroll: req.query.scroll || thread.messages[0].id });
thread.save();
} else
res.error(404, `We don't have any thread with id ${id}.`);
});
module.exports = app;

31
routes/users.js Normal file
View file

@ -0,0 +1,31 @@
const { Router } = require("express");
const app = Router();
const { UserModel, MessageModel, ThreadModel } = require("../models");
app.get("/", async ({ user }, res) => {
const users = await UserModel.find(user?.admin ? {} : { deleted: false });
return res.reply("users", { users })
});
app.get("/:id", async (req, res) => {
const user = req.user
const { id } = req.params;
const member = await UserModel.get(id);
if (member && (user?.admin || !member.deleted)) {
const message = await MessageModel.count({ "author.id": id });
const thread = await ThreadModel.count({ "author.id": id });
member.about = member.about.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;").replaceAll(">", "&gt;")
.replaceAll("\"", "&quot;").replaceAll("'", "&#39;")
.replaceAll("\n", "<br>");
res.reply("user", { member, counts: { message, thread } })
}
else res.error(404, `We don't have any user with id ${id}.`);
});
module.exports = app;

View file

@ -1,92 +0,0 @@
require("dotenv").config();
const
{ def_theme, forum_name, description, limits, global_ratelimit: RLS, discord_auth, host } = require("../config.json"),
{ UserModel, BanModel } = require("./models"),
port = process.env.PORT || 3000,
mongoose = require("mongoose"),
express = require('express'),
fs = require("fs"),
{ join } = require("path"),
app = express(),
{ mw: IP } = require('request-ip'),
{ RL, themes } = require('./lib'),
SES = require('express-session'),
MS = require("connect-mongo"),
DB = mongoose.connect(process.env.MONGO_DB_URL)
.then(async m => {
console.log("Database is connected with", (app.ips = await BanModel.find({})).length, "banned IPs");
return m.connection.getClient()
});
app.ips = [];
app.onlines = new Map();
setInterval(() => {
const now = Date.now();
for (const [ip, lastSeen] of app.onlines.entries())
if (now - lastSeen > 1000 * 60 * 5)
app.onlines.delete(ip);
}, 1000 * 60 * 5);
app.set("view engine", "ejs");
app.set("limits", limits);
for (const theme of themes)
app.use(`/themes/${theme.codename}`, express.static(join(__dirname, "themes", theme.codename, "public")));
app.use(express.static(join(__dirname, "public")), express.json(), express.urlencoded({ extended: true }), IP(),
SES({ secret: process.env.SECRET, store: MS.create({ clientPromise: DB, stringify: false }), resave: false, saveUninitialized: false }),
async (req, res, next) => {
if (app.ips.includes(req.clientIp)) return res.status(403).send("You are banned from this forum.");
const lastSeen = Date.now();
req.user = req.session.userID ? await UserModel.findOneAndUpdate({ id: req.session.userID }, {
lastSeen, $addToSet: { ips: req.clientIp }
}) : null;
app.onlines.set(req.clientIp, lastSeen);
let theme = req.user?.theme || def_theme;
if (!themes.some(t => t.codename === theme.codename))
theme = def_theme;
res.reply = (page, options = {}, status = 200) => {
const road = join(__dirname, "themes", theme.codename, "views", `${page}.ejs`);
const renderpage = fs.existsSync(road) ? road : join(__dirname, "themes", def_theme, "views", `${page}.ejs`);
return res.status(status).render(renderpage, {
dataset: {
themes, theme, forum_name, description,
getFile: file => join(__dirname, "themes", file),
},
user: req.user,
...options
});
}
res.error = (type, error) => res.reply("error", { type, error }, type);
if (req.user?.deleted) {
req.session.destroy();
return res.error(403, "Your account has been deleted.");
}
if (req.user && req.user.state == "APPROVAL" && !req.user.admin && !req.url.startsWith("/auth/email")) return res.error(403, "Your account is not approved yet.");
next();
}
);
if (RLS.enabled) app.use(RL(RLS.windowMs, RLS.max));
if (discord_auth)
app.set("DISCORD_AUTH_URL", `https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_ID}&redirect_uri=${host}%2Fauth%2Fdiscord&response_type=code&scope=identify`);
for (const file of fs.readdirSync(join(__dirname, "routes")))
app.use("/" + file.replace(".js", ""), require(`./routes/${file}`));
app.all("*", (req, res) => res.error(404, "This page does not exist on this forum."));
app.listen(port, () => console.log(`${forum_name} on port:`, port));

View file

@ -1,34 +0,0 @@
const RL = require('express-rate-limit');
const nodemailer = require("nodemailer");
const config = require("../config.json");
const crypto = require("crypto");
const { readdirSync } = require('fs');
const { join } = require('path');
require("dotenv").config();
module.exports = {
themes: readdirSync(join(__dirname, "themes")).map(f => require(`./themes/${f}`)),
threadEnum: ["OPEN", "APPROVAL", "DELETED"],
userEnum: ["ACTIVE", "APPROVAL", "DELETED", "BANNED"],
RL(windowMs = 60_000, max = 1) {
return RL({
windowMs, max, standardHeaders: true, legacyHeaders: false,
handler: (req, res, next, opts) => !req.user?.admin ? res.error(opts.statusCode, "You are begin ratelimited") : next()
})
},
getGravatar(email, size) {
return `https://www.gravatar.com/avatar/${crypto.createHash('md5').update(email).digest("hex")}?d=mp${size ? `&size=${size}` : ''}`;
},
// eslint-disable-next-line no-useless-escape
emailRegEx: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
}
if (config.email_auth)
module.exports.transporter = nodemailer.createTransport({
service: process.env.EMAIL_SERVICE, direct: true, secure: true,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
}
});

View file

@ -1,10 +0,0 @@
const mongoose = require("mongoose")
const schema = new mongoose.Schema({
ip: { type: String, unique: true },
reason: { type: String, default: "No reason given" },
authorID: { type: String }
});
const model = mongoose.model('ban', schema);
module.exports = model;

View file

@ -1,23 +0,0 @@
const mongoose = require("mongoose")
const schema = new mongoose.Schema({
name: { type: String, unique: true },
desp: String, position: Number,
id: { type: String, unique: true },
authorID: { type: String }
});
schema.methods.takeId = async function () {
// eslint-disable-next-line no-use-before-define
this.id = await model.countDocuments() || 0;
return this;
}
schema.methods.getLink = function (id = this.id) {
return "/categories/" + id;
}
const model = mongoose.model('category', schema);
module.exports = model;

View file

@ -1,47 +0,0 @@
const mongoose = require("mongoose");
const cache = require("./cache");
const { limits } = require("../../config.json");
const schema = new mongoose.Schema({
id: { type: String, unique: true },
authorID: {
type: String, get(v) { return v || this.author?.id }
},
author: {
type: Object, set(v) {
this.authorID = v.id;
return v;
}
},
threadID: String,
content: { type: String, maxlength: limits.message },
oldContents: [String],
time: { type: Date, default: Date.now },
deleted: { type: Boolean, default: false },
edited: { type: Boolean, default: false },
react: {
like: [String],
dislike: [String]
}
})
schema.methods.get_author = cache.getAuthor
schema.methods.takeId = async function () {
// eslint-disable-next-line no-use-before-define
this.id = await model.countDocuments() || 0;
return this;
}
schema.methods.getLink = function (id = this.id) {
return "/messages/" + id;
}
const model = mongoose.model('message', schema);
model.get = async id => {
const message = await model.findOne({ id })
return await message.get_author();
};
module.exports = model;

View file

@ -1,68 +0,0 @@
const mongoose = require("mongoose");
const cache = require("./cache")
const MessageModel = require("./Message");
const { limits, default_thread_state } = require("../../config.json");
const { threadEnum } = require("../lib");
const schema = new mongoose.Schema({
id: { type: String, unique: true },
categoryID: String,
authorID: {
type: String, get(v) { return v || this.author?.id }
},
author: {
type: Object, set(v) {
this.authorID = v.id;
return v;
}
},
title: { type: String, maxlength: limits.title },
oldTitles: [String],
time: { type: Date, default: Date.now },
edited: { type: Boolean, default: false },
state: { type: String, default: default_thread_state, enum: threadEnum, uppercase: true },
messages: [String],
views: { type: Number, default: 0 }
});
schema.methods.get_author = cache.getAuthor;
schema.virtual("deleted").get(function () {
return this.state === "DELETED";
}).set(function (value) {
this.set({ state: value ? "DELETED" : "OPEN" });
});
schema.methods.get_category = async function () {
return await require("./Category").findOne({ id: this.categoryID }) || { id: this.categoryID, name: "Unknown" };
}
schema.methods.messageCount = async function (admin = false) {
const query = { threadID: this.id };
if (!admin) query.deleted = false;
return await MessageModel.countDocuments(query) || 0;
};
schema.methods.push = function (messageID) {
this.messages.push(messageID);
return this;
}
schema.methods.takeId = async function () {
// eslint-disable-next-line no-use-before-define
this.id = await model.countDocuments() || 0;
return this;
}
schema.methods.getLink = function (id = this.id) {
return "/threads/" + id;
}
const model = mongoose.model('thread', schema);
model.get = id => model.findOne({ id }).then(thread => thread.get_author());
module.exports = model;

View file

@ -1,51 +0,0 @@
const mongoose = require("mongoose")
const { def_theme, limits, default_user_state } = require("../../config.json");
const { userEnum } = require("../lib");
const schema = new mongoose.Schema({
id: { type: String, unique: true },
discordID: { type: String },
name: { type: String, maxlength: limits.names },
avatar: { type: String, default: "/images/avatars/default.jpg" },
time: { type: Date, default: Date.now },
edited: { type: Boolean, default: false },
about: { type: String, default: "", maxlength: limits.desp },
admin: { type: Boolean, default: false },
theme: {
codename: { type: String, default: def_theme.codename },
language: { type: String, default: def_theme.language }
},
lastSeen: { type: Date, default: Date.now, select: false },
hideLastSeen: { type: Boolean, default: false },
ips: { type: [String], default: [], select: false },
password: { type: String, select: false },
discord_code: { type: String, select: false },
state: { type: String, default: default_user_state, enum: userEnum, uppercase: true },
email: { type: String, select: false },
email_code: { type: String, select: false },
});
schema.virtual("deleted").get(function () {
return this.state === "DELETED";
}).set(function (value) {
this.set({ state: value ? "DELETED" : "ACTIVE" });
});
schema.virtual("active").get(function () {
return this.state === "ACTIVE";
})
schema.methods.takeId = async function () {
// eslint-disable-next-line no-use-before-define
this.id = String(await model.countDocuments() || 0);
return this;
}
schema.methods.getLink = function (id = this.id) {
return "/users/" + id;
}
const model = mongoose.model('user', schema);
model.get = (id, select = "") => model.findOne({ id }, select);
module.exports = model;

View file

@ -1,17 +0,0 @@
const UserModel = require("./User");
const UserCache = new Map();
module.exports.getAuthor = async function () {
const id = this.authorID || this.author?.id;
let user = UserCache.get(id);
if (!user)
UserCache.set(id, user = await UserModel.findOne({ id }));
if (!this.get('authorID', null, { getters: false })) {
this.authorID = user.id;
await this.save();
}
this.author = user;
return this;
}

View file

@ -1,7 +0,0 @@
module.exports = {
CategoryModel: require("./Category"),
MessageModel: require("./Message"),
ThreadModel: require("./Thread"),
UserModel: require("./User"),
BanModel: require("./Ban")
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -1,63 +0,0 @@
function b64toBlob(b64Data, contentType = "", sliceSize = 512) {
const byteCharacters = atob(b64Data);
const byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++)
byteNumbers[i] = slice.charCodeAt(i);
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
const blob = new Blob(byteArrays, {
type: contentType
});
return blob;
}
const image = document.getElementById('image');
const button = document.getElementById('button');
const reader = new FileReader();
document.getElementById("file-input")
.addEventListener("change", function () {
reader.onload = () => {
image.src = reader.result;
let croppable = false;
// eslint-disable-next-line no-undef
const cropper = new Cropper(image, {
aspectRatio: 1,
viewMode: 1,
ready: function () {
croppable = true;
},
});
button.onclick = async () => {
if (!croppable) return;
const croppedCanvas = cropper.getCroppedCanvas();
// const croppedImage = document.getElementById('cropped-image');
const body = new FormData(document.createElement('form'))
const block = croppedCanvas.toDataURL().split(";");
const contentType = block[0].split(":")[1];
const realData = block[1].split(",")[1];
body.append('avatar', b64toBlob(realData, contentType));
// eslint-disable-next-line no-undef
const res = await fetch('/api/users/'+MEM_ID+'/avatar', {
method: 'POST',
body
}).then(res => res.json());
if (res.error) return alert(res.error);
alert('Success!');
location.reload();
};
}
reader.readAsDataURL(event.target.files[0]);
});

View file

@ -1,15 +0,0 @@
function editor( uniqueId, textarea = document.getElementById("textarea")) {
return new SimpleMDE({
autosave: {
enabled: true,
uniqueId,
delay: 1000,
},// width: 100%;
element: textarea,
spellChecker: false
});
}

View file

@ -1,3 +0,0 @@
for (const modal of document.querySelectorAll("[modal]"))
modal.onclick = () => {
document.querySelector(modal.getAttribute("modal")).classList.toggle('active')};

View file

@ -1,84 +0,0 @@
import request from "./request.js";
let editing;
// THREAD:
window.edit_thread = async function (id) {
const title = prompt("Enter new title!");
const res = await request(`/api/threads/${id}/`, "PATCH", { title });
if (res.error) return;
alert(`Thread updated`);
document.getElementById("title").innerHTML = title;
}
window.delete_thread = async function (id) {
const res = await request(`/api/threads/${id}/`, "DELETE");
if (res.error) return;
alert(`Thread deleted`);
location.reload();
}
window.undelete_thread = async function (id) {
const res = await request(`/api/threads/${id}/`, "PATCH", { state: "OPEN" });
if (res.error) return;
alert(`Thread undeleted`);
location.reload();
}
// MESSAGES:
window.send_edit = async function (id) {
const message = document.getElementById(`message-${id}`);
const content = editing.value();
const res = await request(`/api/messages/${id}/`, "PATCH", { content });
if (res.error) return;
alert(`Message updated`);
message.querySelector(".content").innerHTML = converter.makeHtml(res.content);
}
window.edit_message = async function (id) {
const message = document.getElementById(`message-${id}`)
const content = message.querySelector(".content");
content.innerHTML = `
<textarea rows="4" cols="40" id="content"></textarea>
<button onclick="send_edit(${id});" class="btn-primary">Edit!</button>`;
const cnt = message.querySelector("#content");
editing = new SimpleMDE({
element: cnt,
spellChecker: false,
height: "200px"
});
editing.value(content.rawText)
}
window.undelete_message = async function (id) {
const response = await request(`/api/messages/${id}/`, "PATCH", { deleted: false });
if (response.deleted) return;
const message = document.getElementById("message-" + id);
message.querySelector("#deleted").remove();
message.querySelector(".dots-menu").innerHTML = `
<a onclick="delete_message('${id}');">DELETE</a>
<a onclick="edit_message('${id}');">EDIT</a>`
}
window.delete_message = async function (id) {
const response = await request(`/api/messages/${id}/`, "DELETE");
if (!response.deleted) return
const message = document.getElementById(`message-${id}`);
alert("Message deleted");
message.querySelector(".dots-menu").innerHTML = `<a onclick="undelete_message('${id}');">UNDELETE</a>`;
let dots = message.querySelector(".dots");
dots.innerHTML = "<i class='bx bx-trash bx-sm' id='deleted' style='color: var(--important)'></i>" + dots.innerHTML;
}
window.react = async function (id, type) {
const res = await request(`/api/messages/${id}/react/${type}`)
const message = document.getElementById(`message-${id}`);
for (const react in res.react)
message.querySelector("#" + react).innerHTML = res.react[react].length;
}

View file

@ -1,304 +0,0 @@
/*!
* Cropper.js v1.5.12
* https://fengyuanchen.github.io/cropperjs
*
* Copyright 2015-present Chen Fengyuan
* Released under the MIT license
*
* Date: 2021-06-12T08:00:11.623Z
*/
.cropper-container {
direction: ltr;
font-size: 0;
line-height: 0;
position: relative;
-ms-touch-action: none;
touch-action: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.cropper-container img {
display: block;
height: 100%;
image-orientation: 0deg;
max-height: none !important;
max-width: none !important;
min-height: 0 !important;
min-width: 0 !important;
width: 100%;
}
.cropper-wrap-box,
.cropper-canvas,
.cropper-drag-box,
.cropper-crop-box,
.cropper-modal {
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
}
.cropper-wrap-box,
.cropper-canvas {
overflow: hidden;
}
.cropper-drag-box {
background-color: #fff;
opacity: 0;
}
.cropper-modal {
background-color: #000;
opacity: 0.5;
}
.cropper-view-box {
display: block;
height: 100%;
outline: 1px solid #39f;
outline-color: rgba(51, 153, 255, 0.75);
overflow: hidden;
width: 100%;
}
.cropper-dashed {
border: 0 dashed #eee;
display: block;
opacity: 0.5;
position: absolute;
}
.cropper-dashed.dashed-h {
border-bottom-width: 1px;
border-top-width: 1px;
height: calc(100% / 3);
left: 0;
top: calc(100% / 3);
width: 100%;
}
.cropper-dashed.dashed-v {
border-left-width: 1px;
border-right-width: 1px;
height: 100%;
left: calc(100% / 3);
top: 0;
width: calc(100% / 3);
}
.cropper-center {
display: block;
height: 0;
left: 50%;
opacity: 0.75;
position: absolute;
top: 50%;
width: 0;
}
.cropper-center::before,
.cropper-center::after {
background-color: #eee;
content: ' ';
display: block;
position: absolute;
}
.cropper-center::before {
height: 1px;
left: -3px;
top: 0;
width: 7px;
}
.cropper-center::after {
height: 7px;
left: 0;
top: -3px;
width: 1px;
}
.cropper-face,
.cropper-line,
.cropper-point {
display: block;
height: 100%;
opacity: 0.1;
position: absolute;
width: 100%;
}
.cropper-face {
background-color: #fff;
left: 0;
top: 0;
}
.cropper-line {
background-color: #39f;
}
.cropper-line.line-e {
cursor: ew-resize;
right: -3px;
top: 0;
width: 5px;
}
.cropper-line.line-n {
cursor: ns-resize;
height: 5px;
left: 0;
top: -3px;
}
.cropper-line.line-w {
cursor: ew-resize;
left: -3px;
top: 0;
width: 5px;
}
.cropper-line.line-s {
bottom: -3px;
cursor: ns-resize;
height: 5px;
left: 0;
}
.cropper-point {
background-color: #39f;
height: 5px;
opacity: 0.75;
width: 5px;
}
.cropper-point.point-e {
cursor: ew-resize;
margin-top: -3px;
right: -3px;
top: 50%;
}
.cropper-point.point-n {
cursor: ns-resize;
left: 50%;
margin-left: -3px;
top: -3px;
}
.cropper-point.point-w {
cursor: ew-resize;
left: -3px;
margin-top: -3px;
top: 50%;
}
.cropper-point.point-s {
bottom: -3px;
cursor: s-resize;
left: 50%;
margin-left: -3px;
}
.cropper-point.point-ne {
cursor: nesw-resize;
right: -3px;
top: -3px;
}
.cropper-point.point-nw {
cursor: nwse-resize;
left: -3px;
top: -3px;
}
.cropper-point.point-sw {
bottom: -3px;
cursor: nesw-resize;
left: -3px;
}
.cropper-point.point-se {
bottom: -3px;
cursor: nwse-resize;
height: 20px;
opacity: 1;
right: -3px;
width: 20px;
}
@media (min-width: 768px) {
.cropper-point.point-se {
height: 15px;
width: 15px;
}
}
@media (min-width: 992px) {
.cropper-point.point-se {
height: 10px;
width: 10px;
}
}
@media (min-width: 1200px) {
.cropper-point.point-se {
height: 5px;
opacity: 0.75;
width: 5px;
}
}
.cropper-point.point-se::before {
background-color: #39f;
bottom: -50%;
content: ' ';
display: block;
height: 200%;
opacity: 0;
position: absolute;
right: -50%;
width: 200%;
}
.cropper-invisible {
opacity: 0;
}
.cropper-bg {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');
}
.cropper-hide {
display: block;
height: 0;
position: absolute;
width: 0;
}
.cropper-hidden {
display: none !important;
}
.cropper-move {
cursor: move;
}
.cropper-crop {
cursor: crosshair;
}
.cropper-disabled .cropper-drag-box,
.cropper-disabled .cropper-face,
.cropper-disabled .cropper-line,
.cropper-disabled .cropper-point {
cursor: not-allowed;
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,31 +0,0 @@
const { UserModel, ThreadModel, MessageModel, CategoryModel } = require("../models");
const { Router } = require("express");
const app = Router();
app.get("/", async (req, res) => {
const [
categories, users, threads, messages, newestMember, newestMessages, newestThreads, onlineMemberCount, onlineMembers
] = await Promise.all([
CategoryModel.countDocuments(),
UserModel.countDocuments({ deleted: false }),
ThreadModel.countDocuments({ state: "OPEN" }),
MessageModel.countDocuments({ deleted: false }),
UserModel.findOne({ deleted: false }, "name id").sort({ time: -1 }),
MessageModel.find({ deleted: false }).sort({ time: -1 }).limit(10),
ThreadModel.find({ state: "OPEN" }).sort({ time: -1 }).limit(10),
UserModel.countDocuments({ deleted: false, lastSeen: { $gt: Date.now() - 1000 * 60 * 5 } }),
UserModel.find({ deleted: false, hideLastSeen: false, lastSeen: { $gt: Date.now() - 1000 * 60 * 5 } }, "name id")
]),
onlineTotal = req.app.onlines.size,
onlineGuests = onlineTotal - onlineMemberCount;
res.reply("index", {
categories, users, threads, messages,
newestMember, newestMessages, newestThreads,
onlineMemberCount, onlineMembers, onlineGuests, onlineTotal
});
});
module.exports = app;

View file

@ -1,15 +0,0 @@
const { Router } = require("express");
const fs = require("fs");
const { BanModel, UserModel } = require("../models");
const app = Router();
app.use(async (req, res, next) => {
if (!req.user?.admin) return res.error(403, "You are not admin");
next();
});
app.get("/", async (req, res) => {
res.reply("admin", { bans: await BanModel.find({}), admins: await UserModel.find({ admin: true }) });
});
app.get("/config", async (req, res) => {
res.reply("config", { config: fs.readFileSync("./config.json", "utf8") });
});
module.exports = app;

View file

@ -1,39 +0,0 @@
const { Router } = require("express");
const app = Router();
const fs = require("fs");
const bcrypt = require("bcrypt");
const { UserModel } = require("../../models");
const{join}=require("path");
app.use(async (req, res, next) => {
res.error = (status, error) => res.status(status).json({ error });
res.complate = result => res.status(200).json(result);
if (req.user) return next();
const authHeader = req.headers.authorization;
if (!authHeader) return res.error(401, "No authorization header");
const [name, password] = Buffer.from(authHeader.split(' ')[1], "base64").toString().split(":");
if (!name || !password)
return res.error(400, "Authorise headers are not well formed");
const user = await UserModel.findOne({ name });
if (!user || user.deleted) return res.error(401, `We don't have any user with name ${name}.`)
if (!user.active) return res.error(401, "Your account is not approved yet.");
if (!await bcrypt.compare(password, user.password)) return res.error(401, 'Incorrect Password!');
req.user = user;
next();
});
app.get("/me", (req, res) => res.complate(req.user))
for (const file of fs.readdirSync(join(__dirname, "routes")))
app.use("/" + file.replace(".js", ""), require(`./routes/${file}`));
app.all("*", (req, res) => res.error(400, "Bad request"));
module.exports = app;

View file

@ -1,38 +0,0 @@
const { BanModel } = require("../../../models");
const { Router } = require("express")
const app = Router();
app.use((req, res, next) => {
if (!req.user.admin) return res.error(403, "You have not got permission for this.");
next();
});
app.get("/", async (req, res) => {
const bans = await BanModel.find({});
res.complate(bans);
});
app.get("/:ip", async (req, res) => {
const ban = await BanModel.findOne({ ip: req.params.ip });
if (!ban) return res.error(400, "This ip is not banned.");
res.complate(ban);
});
app.post("/:ip", async (req, res) => {
if (await BanModel.exists({ ip: req.params.ip })) return res.error(400, "This ip is already banned.");
const ban = await BanModel.create({ ip: req.params.ip, reason: req.query.reason || "No reason given", authorID: req.user.id });
req.app.ips.push(req.params.ip);
res.complate(ban);
});
app.delete("/:ip/", async (req, res) => {
if (!await BanModel.exists({ ip: req.params.ip })) return res.error(400, "This ip is already not banned.");
res.complate(await BanModel.deleteOne({ ip: req.params.ip }));
});
module.exports = app;

View file

@ -1,44 +0,0 @@
const { CategoryModel } = require("../../../models");
const { Router } = require("express")
const app = Router();
app.use((req, res, next) => {
if (!req.user.admin) return res.error(403, "You have not got permission for this.");
next();
});
app.param("id", async (req, res, next, id) => {
req.category = await CategoryModel.findOne({ id });
if (!req.category) return res.error(404, `We don't have any category with id ${id}.`);
next();
});
app.get("/", async (req, res) => {
const categories = await CategoryModel.find({});
res.complate(categories);
});
app.get("/:id", async (req, res) => res.complate(req.category));
app.patch("/:id", async (req, res) => {
const { category } = req;
if (req.body.name) category.name = req.body.name;
if (req.body.desp) category.name = req.body.name;
res.complate(await category.save());
});
app.delete("/:id", async (req, res) => res.complate(await CategoryModel.deleteOne({ id: req.params.id })));
app.post("/", async (req, res) => {
const { name, desp } = req.body;
if (!name) return res.error(400, "You have to give a name for the category.");
if (await CategoryModel.exists({ name })) return res.error(400, "This category is already opened.");
const category = await new CategoryModel({ name, desp, authorID: req.user.id }).takeId();
res.complate(await category.save());
});
module.exports = app;

View file

@ -1,25 +0,0 @@
const { Router } = require("express")
const fs = require("fs");
const app = Router();
app.use((req, res, next) => {
if (!req.user.admin)
return res.error(403, "You have not got permission for this.");
next();
});
app.get("/", (req, res) => {
try {
return res.reply(JSON.parse(fs.readFileSync("./config.json", "utf8")));
} catch (e) {
res.error(500, e.message);
}
});
app.put("/", (req, res) => {
const write = req.query.text ? req.body : JSON.stringify(req.body, null, 4)
fs.writeFileSync("./config.json", write);
require.cache[require.resolve("../../../../config.json")] = require("../../../../config.json");
res.complate(require("../../../../config.json"));
});
module.exports = app;

View file

@ -1,113 +0,0 @@
const { MessageModel, ThreadModel } = require("../../../models");
const { RL } = require('../../../lib');
const { Router } = require("express")
const app = Router();
app.param("id", async (req, res, next, id) => {
req.message = await MessageModel.get(id);
if (!req.message) return res.error(404, `We don't have any message with id ${id}.`);
if (req.message.deleted && !req.user?.admin)
return res.error(404, `You do not have permissions to view this message with id ${id}.`)
next();
});
app.get("/:id", async (req, res) => res.complate(req.message));
app.delete("/:id/", async (req, res) => {
const { message, user } = req;
if (user.id != message.authorID && !user.admin)
return res.error(403, "You have not got permission for this.");
if (message.deleted) return res.error(404, "This message is already deleted.");
message.deleted = true;
await message.save()
res.complate(message);
});
app.patch("/:id/", async (req, res) => {
const { message, user } = req;
if (user.id !== message.authorID && !user.admin) return res.error(403, "You have not got permission for this.");
if (!Object.keys(req.body).some(Boolean)) return res.error(400, "Missing message informations for update in request body.");
const { content, deleted } = req.body;
const limits = req.app.get("limits");
if (content.length < 5 || content.length > limits.message) return res.error(400, `content must be between 5 - ${limits.message} characters`);
if (deleted === false) message.deleted = false;
message.content = content;
if (!message.oldContents.includes(content))
message.oldContents.push(content);
message.edited = true;
await message.save();
res.complate(message);
})
app.post("/", RL(), async (req, res) => {
const { threadID, content } = req.body;
if (!content) return res.error(400, "Missing message content in request body.");
const limits = req.app.get("limits");
if (content.length < 5 || content.length > limits.message) return res.error(400, `content must be between ${limits.message} characters`);
const thread = await ThreadModel.get(threadID);
if (!thread) return res.error(404, `We don't have any thread with id ${threadID}.`);
const message = await new MessageModel({ content, author: req.user, threadID: thread.id }).takeId();
await message.save();
await thread.push(message.id).save();
res.complate(message);
})
app.post("/:id/react/:type", async (req, res) => {
const { message } = req;
if (req.params.type == "like") {
if (message.react.like.includes(req.user.id))
message.react.like.pull(req.user.id);
else {
message.react.like.push(req.user.id);
message.react.dislike.pull(req.user.id);
}
} else if (req.params.type == "dislike") {
if (message.react.dislike.includes(req.user.id))
message.react.dislike.pull(req.user.id);
else {
message.react.dislike.push(req.user.id);
message.react.like.pull(req.user.id);
}
} else
return res.error(400, `We don't have any react type with name ${req.params.type}.`);
await message.save();
res.complate(message);
});
module.exports = app;

View file

@ -1,40 +0,0 @@
const { Router } = require("express")
const { MessageModel, ThreadModel, UserModel } = require("../../../models");
const app = Router();
app.use((req, res, next) => {
req.sq = {}
req.so = {}
const limit = Number(req.query.limit);
const skip = Number(req.query.skip);
if (!req.user.admin) req.sq.deleted = false;
if (limit) req.so.limit = limit;
if (skip) req.so.skip = skip;
next();
})
app.get("/users", async (req, res) => {
if (!req.query.q) return res.error(400, "Missing query parameter 'q' in request body.");
const results = await UserModel.find({ ...req.sq, name: { $regex: req.query.q, $options: "i" } }, null, req.so);
res.complate(results);
});
app.get("/messages", async (req, res) => {
if (!Object.values(req.query).length) return res.error(400, "Missing query parameters in request body.");
const query = { ...req.sq };
if (req.query.q) query.content = { $regex: req.query.q, $options: "i" };
if (req.query.authorID) query.authorID = req.query.authorID;
const results = await MessageModel.find(query, null, req.so)
res.complate(results);
});
app.get("/threads", async (req, res) => {
if (!Object.values(req.query).length) return res.error(400, "Missing query parameters in request body.");
const query = {};
if (!req.user.admin) query.state = "OPEN";
if (req.query.q) query.title = { $regex: req.query.q, $options: "i" };
if (req.query.authorID) query.authorID = req.query.authorID;
const results = await ThreadModel.find(query, null, req.so)
res.complate(results);
});
module.exports = app;

View file

@ -1,15 +0,0 @@
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,111 +0,0 @@
const { MessageModel, ThreadModel } = require("../../../models");
const { Router } = require("express")
const { RL, threadEnum } = require('../../../lib');
const app = Router();
app.param("id", async (req, res, next, id) => {
req.thread = await ThreadModel.get(id);
if (!req.thread) return res.error(404, `We don't have any thread with id ${id}.`);
if (req.thread.state !== "OPEN" && !req.user?.admin)
return res.error(404, `You do not have permissions to view this thread with id ${id}.`)
next();
});
app.get("/:id", async (req, res) => res.complate(req.thread));
app.get("/:id/messages/", async (req, res) => {
const { id } = req.params;
const limit = Number(req.query.limit);
const skip = Number(req.query.skip);
const query = { threadID: id };
if (!req.user.admin) query.deleted = false;
const options = { sort: { time: -1 } };
if (limit) options.limit = limit;
if (skip) options.skip = skip;
const messages = await MessageModel.find(query, null, options)
if (!messages.length) return res.error(404, "We don't have any messages in this with your query thread.");
res.complate(messages);
})
app.post("/", RL(5 * 60_000, 1), async (req, res) => {
const { title, content, category } = req.body;
if (!content || !title) return res.error(400, "Missing content/title in request body.");
const limits = req.app.get("limits");
if (title.length < 5 || title.length > limits.title) return res.error(400, `title must be between 5 - ${limits.title} characters`);
if (content.length < 5 || content.length > limits.message) return res.error(400, `content must be between 5 - ${limits.message} characters`);
const { user } = req;
const thread = await new ThreadModel({ title, author: user }).takeId()
if (category)
thread.categoryID = category;
const message = await new MessageModel({ content, author: user, threadID: thread.id }).takeId()
await thread.push(message.id).save();
await message.save();
res.complate(thread);
});
app.patch("/:id/", async (req, res) => {
const { user, thread } = req;
if (user.id !== thread.authorID && !user.admin) return res.error(403, "You have not got permission for this.");
if (!Object.values(req.body).some(Boolean)) return res.error(400, "Missing thread informations for update in request body.");
const { title, state } = req.body;
if (title) {
const limits = req.app.get("limits");
if (title.length < 5 || title.length > limits.title) return res.error(400, `title must be between 5 - ${limits.title} characters`);
if (thread.oldTitles.at(-1) == title) return res.error(400, "You can't use the same title as the previous one.");
thread.oldTitles.push(thread.title = title);
}
if (state) {
if (!user.admin)
return res.error(403, "You have not got permission for change state.");
if (thread.state === state) return res.error(400, "You can't change thread state to same state.");
if (!threadEnum.includes(state)) return res.error(400, "Invalid thread state.");
if (thread.deleted)
await MessageModel.updateMany({ threadID: thread.id }, { deleted: false });
thread.state = state;
}
await thread.save();
res.complate(thread);
})
app.delete("/:id/", async (req, res) => {
const { user, thread } = req;
if (user.id != thread.authorID && !user.admin)
return res.error(403, "You have not got permission for this.");
if (thread.deleted) return res.error(404, "This thread is already deleted.");
thread.deleted= true;
await thread.save();
await MessageModel.updateMany({ threadID: thread.id }, { deleted: true });
res.complate(thread);
})
module.exports = app;

View file

@ -1,112 +0,0 @@
const { UserModel, BanModel } = require("../../../models");
const { Router } = require("express");
const multer = require("multer");
const { themes, emailRegEx } = require("../../../lib")
const app = Router();
const { join } = require("path");
app.param("id", async (req, res, next, id) => {
if (id === "me") //
id = req.user.id;
req.member = await UserModel.get(id, req.user.admin ? "+lastSeen +ips" : "");
if (!req.member) return res.error(404, `We don't have any user with id ${id}.`);
if (req.member.deleted && !req.user?.admin)
return res.error(404, `You do not have permissions to view this user with id ${id}.`);
next();
});
app.get("/:id", async (req, res) => res.complate(req.member));
app.delete("/:id", async (req, res) => {
const { user, member } = req;
if (!user.admin)
return res.error(403, "You have not got permission for this.");
if (member.deleted) return res.error(404, `This user is with id ${member.id} already deleted.`);
member.deleted = true;
await member.save();
res.complate(member);
});
app.patch("/:id", async (req, res) => {
const { user, member } = req;
if (req.user.id !== member.id && !user.admin) return res.error(403, "You have not got permission for this.");
if (!Object.keys(req.body).some(Boolean)) return res.error(400, "Missing member informations in request body.");
const { name, about, admin, deleted, hideLastSeen, theme, email } = req.body;
if ((admin?.length || "deleted" in req.body) && !req.user.admin) return res.error(403, "You have not got permission for edit 'admin' and 'deleted' information, or bad request.");
const { names, desp } = req.app.get("limits");
if (name) {
if (name.length < 3 || names > 25) return res.error(400, `Username must be between 3 - ${names} characters`);
member.name = name;
}
if (about) {
if (about.length > desp) return res.error(400, `About must be under ${desp} characters`);
member.about = about;
}
if (theme && themes.some(t => t.codename === theme.codename))
member.theme = theme;
if (email) {
if (!emailRegEx.test(email)) return res.error(400, "E-mail is not valid");
if (await UserModel.exists({ email })) return res.error(400, "E-mail is already in use");
member.email = email;
}
if (typeof admin === "boolean" || ["false", "true"].includes(admin)) member.admin = admin;
if (deleted === false) member.deleted = false;
if (typeof hideLastSeen === "boolean" || ["false", "true"].includes(admin)) member.hideLastSeen = hideLastSeen;
member.edited = true;
res.complate(await member.save());
})
app.post("/:id/ban", async (req, res) => {
if (!req.user.admin) return res.error(403, "You have not got permission for this.");
const { member } = req;
for (const ip of member.ips)
try {
await BanModel.create({ ip, reason: `Ban for ${member.name}`, authorID: req.user.id });
req.app.ips.push(ip);
} catch {
continue;
}
res.complate(member);
});
const storage = multer.diskStorage({
destination: join(__dirname, "..", "..", "..", "public", "images", "avatars"),
filename: function (req, _file, cb) {
cb(null, req.member.id + ".jpg")
}
})
const upload = multer({ storage })
app.post("/:id/avatar", upload.single('avatar'), async (req, res) => {
const { member } = req;
if (req.user.id !== member.id && !req.user.admin) return res.error(403, "You have not got permission for this.");
if (!req.file) return res.error(400, "Missing avatar in request body.");
member.avatar = "/images/avatars/" + req.file.filename;
res.complate(await member.save());
});
module.exports = app;

View file

@ -1,98 +0,0 @@
const { Router } = require("express")
const { UserModel } = require("../models");
const app = Router();
const { host, email_auth } = require("../../config.json")
app.get("/discord", async (req, res) => {
const client_id = process.env.DISCORD_ID;
if (!client_id) return res.error(404, "Discord auth is disabled")
const { code } = req.query;
if (!code) return res.error(400, "No code provided");
try {
const response = await fetch('https://discord.com/api/v10/oauth2/token', {
method: 'POST',
body: new URLSearchParams({
client_id, code,
client_secret: process.env.DISCORD_SECRET,
grant_type: 'authorization_code',
redirect_uri: host + "/auth/discord",
scope: 'identify+email',
}).toString(),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
if (!response.ok) return res.error(500, "Bad request to discord");
const { access_token, token_type } = await response.json();
const discord = await fetch('https://discord.com/api/users/@me', {
headers: { authorization: `${token_type} ${access_token}` }
}).then(res => res.json());
const forum = await UserModel.findOne({ discordID: discord.id });
if (req.user) {
if (req.user.discordID)
return res.error(403, "Your forum account is already linked to a discord account.");
if (forum)
return res.error(409, "This discord account is already linked to a forum account.");
req.user.discordID = discord.id;
req.user.discord_code = code;
await req.user.save();
return res.redirect(`/users/${req.user.id}`)
}
if (forum) {
req.session.userID = forum.id;
return res.redirect("/");
}
let name = discord.username;
while (await UserModel.exists({ name }))
name += Math.floor(Math.random() * 2);
const user2 = new UserModel({
name, email: discord.email,
discordID: discord.id, discord_code: code,
avatar: `https://cdn.discordapp.com/avatars/${discord.id}/${discord.avatar}.png?size=256`,
});
await user2.takeId();
await user2.save();
req.session.userID = user2.id;
res.redirect("/");
} catch (error) {
res.error(500, "Something went wrong");
console.error(error);
}
});
app.delete("/discord", async (req, res) => {
if (!req.user) return res.error(401, "You are not logged in");
if (!req.user.discordID) return res.error(403, "You don't have a discord account linked to your forum account.");
req.user.discordID = undefined;
req.user.discord_code = undefined;
await req.user.save();
res.send("Your discord account has been unlinked from your forum account.");
});
app.get("/email", async (req, res) => {
if (!email_auth) return res.error(404, "Email auth is disabled");
if (!req.user) return res.error(401, "You are not logged in");
if (req.user.email) return res.error(403, "You already have an email linked to your account.");
const { code } = req.query;
if (!code) return res.error(400, "No code provided");
if (code !== req.user.email_code) return res.error(403, "Invalid code");
req.user.state = "ACTIVE";
await req.user.save();
res.send("Your email has been linked to your forum account.");
});
module.exports = app;

View file

@ -1,30 +0,0 @@
const { CategoryModel, ThreadModel } = require("../models");
const { Router } = require("express");
const app = Router();
app.get("/", async (req, res) => {
const page = Number(req.query.page) || 0;
const categories = await CategoryModel.find({}).limit(10).skip(page * 10).sort({ time: -1 });
res.reply("categories", {
categories, page,
pages: Math.ceil(await CategoryModel.countDocuments({}) / 10)
});
});
app.get("/create", (req, res) => res.reply("create_category"));
app.get("/:id", async (req, res) => {
const category = await CategoryModel.findOne({ id: req.params.id });
if (!category) return res.error(404, "Category not found.");
const page = Number(req.query.page) || 0;
const query = { categoryID: category.id };
if (!req.user?.admin) query.deleted = false;
let threads = await ThreadModel.find(query).limit(10).skip(page * 10).sort({ time: -1 });
threads = await Promise.all(threads.map(thread => thread.get_author()));
res.reply("threads", { threads, page, title: `Threads in ${category.name}`, desp: category.desp, pages: Math.ceil(await ThreadModel.countDocuments(query) / 10) });
});
module.exports = app;

View file

@ -1,27 +0,0 @@
const { UserModel } = require("../models");
const { Router } = require("express");
const app = Router();
const bcrypt = require("bcrypt");
app.get("/", (req, res) => res.reply("login", { redirect: req.query.redirect, user: null, discord: req.app.get("DISCORD_AUTH_URL") }));
app.post("/", async (req, res) => {
req.session.userID = null;
const { name, password } = req.body;
if (!name || !password) return res.error(400, "You forgot entering some values")
const member = await UserModel.findOne({
$or: [{ name }, { email: name }]
}, "+password");
if (!member || member.deleted) return res.error(401, 'Incorrect username or email!');
if (!await bcrypt.compare(password, member.password)) return res.error(401, 'Incorrect password!');
req.session.userID = member.id;
res.redirect(req.query.redirect || '/');
});
module.exports = app;

View file

@ -1,64 +0,0 @@
const { UserModel } = require("../models");
const { Router } = require("express")
const bcrypt = require("bcrypt");
const { RL, transporter, emailRegEx, getGravatar } = require('../lib');
const app = Router();
const { email_auth, forum_name, host } = require("../../config.json");
app.get("/", (req, res) => res.reply("register", { user: null, discord: req.app.get("DISCORD_AUTH_URL"), mail: email_auth }));
app.post("/", RL(24 * 60 * 60_000, 5), async (req, res) => {
req.session.userID = null;
let { name, password, about, email } = req.body;
if (!name || !password || !email) return res.error(400, "You forgot entering some values");
if (!emailRegEx.test(email)) return res.error(400, "E-mail is not valid");
const { names } = req.app.get("limits");
if (name.length < 3 || name.length > names) return res.error(400, "Name must be between 3 - 25 characters");
if (password.length < 3 || password.length > names) return res.error(400, "Password must be between 3 - 25 characters");
if (await UserModel.exists({ name })) return res.error(400, `We have got an user named ${name}!`)
if (await UserModel.exists({ email })) return res.error(400, "E-mail is already in use");
const user = new UserModel({ name, email });
user.avatar = getGravatar(email, 128);
if (about) {
if (about.length > 256) return res.error(400, "about must be under 256 characters");
user.about = about;
}
await user.takeId();
if (user.id === "0")
user.admin = true;
else if (email_auth) {
user.email_code = await bcrypt.hash(`${Date.now()}-${Math.floor(Math.random() * 1e20)}`, 10)
transporter.sendMail({
from: transporter.options.auth.user,
to: email,
subject: name + ", please verify your email",
html: `
<h1>Verify your email in ${forum_name}</h1>
<a href="${host}/auth/email?code=${user.email_code}">Click here to verify your email</a>
`
}, (err) => {
if (err) return res.error(500, "Failed to send email");
});
}
user.password = await bcrypt.hash(password, 10);
await user.save();
req.session.userID = user.id;
res.redirect('/');
});
module.exports = app;

View file

@ -1,53 +0,0 @@
const { UserModel, ThreadModel, MessageModel } = require("../models")
const { Router } = require("express");
const app = Router();
app.get("/", (req, res) => res.reply("search"));
app.use(async (req, res, next) => {
req.sq = {}
req.page = Number(req.query.page) || 0;
req.so = { limit: 10, skip: req.page * 10 }
if (!req.user?.admin) req.sq.deleted = false;
next();
});
app.get("/users", async (req, res) => {
if (!req.query.q) return res.error(400, "Missing query parameter 'q' in request body.");
const users = await UserModel.find({ ...req.sq, name: { $regex: req.query.q, $options: "i" } }, null, req.so)
res.reply("users", {
users, page: req.page,
pages: Math.ceil(await UserModel.countDocuments(req.sq) / 10)
});
});
app.get("/messages", async (req, res) => {
if (!Object.values(req.query).length) return res.error(400, "Missing query parameters in request body.");
const query = { ...req.sq };
if (req.query.q) query.content = { $regex: req.query.q, $options: "i" };
if (req.query.authorID) query.authorID = req.query.authorID;
const messages = await MessageModel.find(query, null, req.so)
res.reply("messages", {
messages, page: req.page,
pages: Math.ceil(await MessageModel.countDocuments(query) / 10)
});
});
app.get("/threads", async (req, res) => {
if (!Object.values(req.query).length) return res.error(400, "Missing query parameters in request body.");
const query = {};
if (!req.user?.admin) query.state = "OPEN";
if (req.query.q) query.title = { $regex: req.query.q, $options: "i" };
if (req.query.authorID) query.authorID = req.query.authorID;
const threads = await ThreadModel.find(query, null, req.so)
res.reply("threads", {
threads, page: req.page, title: `Threads with query '${req.query.q}'`,
pages: Math.ceil(await ThreadModel.countDocuments(query) / 10), desp: `${threads.length} threads are listed`
});
});
module.exports = app;

View file

@ -1,28 +0,0 @@
const { UserModel } = require("../models");
const { Router } = require("express")
const bcrypt = require("bcrypt");
const { RL} = require('../lib');
const app = Router();
app.use(async (req, res, next) => {
if (!req.user) return res.error(403, "You are not logged in");
next();
});
app.get("/", (req, res) => res.reply("security"));
app.post("/", RL(24 * 60 * 60_000, 5), async (req, res) => {
let { old_password, password } = req.body;
if (!old_password || !password) return res.error(400, "You forgot entering some values");
const { names } = req.app.get("limits");
if (password.length < 3 || password.length > names) return res.error(400, "Password must be between 3 - 25 characters");
const user = await UserModel.get(req.user.id, "+password");
if (!await bcrypt.compare(old_password, user.password)) return res.error(401, 'Incorrect password!');
user.password = await bcrypt.hash(password, 10);
await user.save();
res.send("Password changed");
});
module.exports = app;

View file

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

View file

@ -1,43 +0,0 @@
const { Router } = require("express");
const app = Router();
const { ThreadModel, MessageModel, CategoryModel } = require("../models")
app.get("/", async (req, res) => {
const page = Number(req.query.page) || 0;
const query = req.user?.admin ? {} : { state: "OPEN" };
let threads = await ThreadModel.find(query).limit(10).skip(page * 10).sort({ time: -1 });
threads = await Promise.all(threads.map(thread => thread.get_author()));
return res.reply("threads", { threads, page, title: "Threads", desp: threads.length + " threads are listed", pages: Math.ceil(await ThreadModel.countDocuments(query) / 10) });
});
app.get("/create/", async (req, res) => res.reply("create_thread", { categories: await CategoryModel.find() }));
app.get("/:id/", async (req, res) => {
const { user, params: { id } } = req
const page = Number(req.query.page || 0);
const thread = await ThreadModel.get(id)
if (thread && (user?.admin || thread.state == "OPEN")) {
thread.count = await thread.messageCount(user?.admin);
thread.pages = Math.ceil(thread.count / 10);
thread.views++;
const query = { threadID: id };
if (!user || !user.admin) query.deleted = false;
const messages = await Promise.all(await MessageModel.find(query).sort({ time: 1 }).limit(10).skip(page * 10)
.then(messages => messages.map(message => message.get_author())));
res.reply("thread", { page, thread, messages });
thread.save();
} else
res.error(404, `We don't have any thread with id ${id}.`);
});
module.exports = app;

View file

@ -1,50 +0,0 @@
const { Router } = require("express");
const app = Router();
const { UserModel, MessageModel, ThreadModel } = require("../models");
app.get("/", async (req, res) => {
const page = Number(req.query.page) || 0;
const query = req.user?.admin ? {} : { deleted: false };
let users = await UserModel.find(query).limit(10).skip(page * 10);
return res.reply("users", { users, page, pages: Math.ceil(await UserModel.countDocuments(query) / 10) });
});
app.get("/:id/avatar", async (req, res) => {
if (!req.user || (!req.user.admin && req.params.id !== req.user.id)) return res.error(403, "You have not got permission for this.");
const member = await UserModel.get(req.params.id);
if (member && (req.user?.admin || !member.deleted))
res.reply("avatar_upload", { member })
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;
const member = await UserModel.get(id, "+lastSeen +ips");
if (member && (user?.admin || !member.deleted)) {
const message = await MessageModel.countDocuments({ authorID: id });
const thread = await ThreadModel.countDocuments({ authorID: id });
res.reply("user", { member, counts: { message, thread }, discord: req.app.get("DISCORD_AUTH_URL") })
}
else res.error(404, `We don't have any user with id ${id}.`);
});
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;

View file

@ -1,6 +0,0 @@
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"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,650 +0,0 @@
@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);
}
.title {
color: var(--main);
}
.input {
padding: 8px 10px;
color: var(--input-clr);
width: 100%;
margin-bottom: 10px;
border: 2px solid var(--borders);
}
form.post {
display: flex;
flex-direction: column;
align-items: center;
max-width: 1000px;
margin: 0 auto;
}
@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;
}
/************************************
Threads
*************************************/
.threads {
width: 100%;
padding: 20px;
}
.threads-box {
width: 100%;
padding: 8px;
box-shadow: 0 0 5px 0 var(--box-shadow);
display: flex;
justify-content: space-between;
margin: 0px 0px 8px 0px;
cursor: pointer;
align-items: center;
}
.threads-box:hover {
background-color: var(--box-shadow);
}
.thread-box-title {
padding: 10px;
font-size: 18px;
font-weight: 700;
color: var(--anti);
}
.thread-box-title>span {
color: var(--important);
}
@media (max-width: 480px) {
.threads-box {
flex-direction: column;
}
}
/*****
LOGIN / REGISTER
******/
form.login {
display: flex;
align-items: center;
flex-direction: column;
max-width: 500px;
margin: 0 auto;
padding: 8px;
}
/********
MESSAGE
********/
.message .date {
color: var(--second);
}
.message {
max-width: 800px;
box-shadow: 0 0 5px 0 var(--box-shadow);
margin: 10px auto;
padding: 20px;
display: flex;
gap: 10px;
position: relative;
}
.message .left {
text-align: center;
border-right: 2px solid var(--borders);
}
.message .left img {
width: 100px;
height: 100px;
border-radius: 50%;
margin-right: 5px;
}
.message .left .username a {
color: var(--t-username);
}
.message .content {
width: 70%;
color: var(--anti);
}
@media(max-width:980px) {
.message {
margin: 10px 20px;
max-width: 100%;
flex-direction: column;
position: relative;
}
.message .left {
padding: 40px;
border: none;
border-bottom: 2px solid var(--borders);
}
.message .left img {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 5px;
position: absolute;
left: 20px;
top: 20px;
}
.message .left .username {
position: absolute;
top: 20px;
left: 80px;
}
.message .date:nth-of-type(3) {
position: absolute;
right: 20px;
top: 20px;
}
.message .date:nth-of-type(2) {
position: absolute;
right: 20px;
top: 40px;
}
.message .content {
width: 50%;
}
}
/************************************
SEARCH
***************************************/
.search form {
display: flex;
align-items: center;
flex-direction: column;
max-width: 500px;
margin: 20px auto;
width: 100%;
padding: 10px;
box-shadow: 0 0 5px 0 #b0b0b0;
}
/****************************************
USERS
*****************************************/
.users {
width: 100%;
padding: 20px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 10px;
}
.user-box {
width: 100%;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 0 5px 0 var(--box-shadow);
max-width: 500px;
}
.user-box-title {
padding: 10px;
margin: 8px;
font-weight: 500;
}
.user-box-title>a {
color: var(--anti);
}
.user-box-title>span {
color: var(--important);
}
.user-box-img {
width: 80px;
height: 80px;
margin: auto;
}
@media (max-width: 992px) {
.users {
display: block;
}
.user-box {
margin-bottom: 10px;
}
}
/************************************
THREAD
*************************************/
.title {
color: var(--input-clr);
font-weight: 700;
font-size: 36px;
}
.reactions {
position: absolute;
right: 20px;
bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.reactions div {
display: flex;
align-items: center;
gap: 5px;
padding: 4px;
color: var(--second);
cursor: pointer;
transition: color 0.3s ease;
}
.reactions div:hover {
color: var(--anti);
}
.reactions div i {
font-size: 22px;
}
.dots {
position: absolute;
right: 20px;
font-size: 22px;
top: 10px;
color: var(--second);
cursor: pointer;
}
.dots-menu {
position: absolute;
top: 50px;
right: 0;
background-color: var(--second);
width: 100px;
text-align: center;
display: none;
}
.active {
display: block;
}
.no-display {
display: none;
}
.dots-menu a {
display: block;
margin: 8px;
cursor: pointer;
}
.dots-menu a:hover:nth-child(1) {
color: var(--important);
}
.dots-menu a:hover:nth-child(2) {
color: var(--main);
}
/* Media Query */
@media(max-width:980px) {
.dots {
position: absolute;
right: auto;
top: 70px;
left: 40px;
}
.dots-menu {
position: absolute;
left: 70px;
top: 60px;
width: 120px;
}
.active {
display: flex;
}
}
/******************************
USER
******************************/
.usercontent {
display: flex;
flex-direction: column;
box-shadow: 0 0 5px 0 var(--box-shadow);
max-width: 900px;
margin: 0 auto;
padding: 10px;
color: var(--anti);
}
.userbox {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 auto;
max-width: 800px;
width: 100%;
}
.userbox-title {
font-weight: 400;
color: var(--anti);
}
.userbox-value {
font-weight: 300;
background-color: var(--main);
color: white;
font-size: 14px;
padding: 4px;
border-radius: 5px;
min-width: 50px;
text-align: center;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View file

@ -1,124 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Admin Panel!" }) %>
<body style="text-align: center;">
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
<style>
table {
font-family: arial, sans-serif;
border-collapse: collapse;
width: 100%;
}
td,
th {
border: 1px solid #dddddd;
text-align: left;
padding: 8px;
color: var(--anti);
}
tr:nth-child(even) {
background-color: #dddddd;
}
</style>
<h2>Welcome to the admin panel of the forum, <%= user.name %>!</h1>
<div>
<!-- Modal -->
<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Banned users</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" onclick="document.getElementById('exampleModal').style.display = 'none';">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<table>
<tr>
<th>IP</th>
<th>Reason</th>
<th>AuthorID</th>
</tr>
<% for (const ban of bans) { %>
<tr>
<td><%=ban.ip%></td>
<td><%=ban.reason%></td>
<td><%=ban.authorID%></td>
</tr>
<% } %>
</table>
</div>
<div class="modal-footer">
<a class="btn-primary" onclick="ban();">IP BAN</a>
<a class="btn-outline-primary" onclick="unban();">REMOVE IP BAN</a>
</div>
</div>
</div>
</div>
<button onclick="window.location.href = '/categories/create';" class="btn-primary">Create Category</button>
<button onclick="window.location.href = '/admin/config';" class="btn-primary">Edit config</button>
<button onclick="document.getElementById('exampleModal').style.display = 'block';" class="btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#exampleModal" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
Banned users
</button>
<div>
<table>
<tr>
<th>Admin list</th>
</tr>
<% for (const admin of admins) { %>
<tr>
<td><a style="color: var(--anti);" href="<%= admin.getLink() %>"><%= admin.name %></a></td>
</tr>
<% } %>
</table>
<ul>
</ul>
</div>
<script type="module">
import request from "../../js/request.js";
window.unban = async function() {
const ip = prompt("Enter ip to unban");
if (!ip) return;
const response = await request("/api/bans/" + ip, "DELETE");
if (response)
alert("IP unbanned!");
else
alert("IP is not unbanned!");
location.reload();
}
window.ban = async function() {
const ip = prompt("Enter ip to ban");
if (!ip) return;
const response = await request("/api/bans/" + ip);
if (response)
alert("IP banned!");
else
alert("IP is not banned!");
location.reload();
}
</script>
</div>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>
</html>

View file

@ -1,55 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Avatar Upload Panel!" }) %>
<body style="text-align: center;">
<link rel="stylesheet" href="/libs/cropper/cropper.css">
<style>
.container {
margin: 20px auto;
max-width: 640px;
}
img {
max-width: 100%;
}
.cropper-view-box,
.cropper-face {
border-radius: 50%;
}
.cropper-view-box {
outline: 0;
box-shadow: 0 0 0 1px #39f;
}
</style>
<script src="/libs/cropper/cropper.js"></script>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
<div class="container">
<h1>Upload avatar for <%= member.name %></h1>
<input type="file" id="file-input">
<img id="image" src="<%= member.avatar %>">
<p>
<button type="button" id="button">Upload</button>
</p>
<file id="cropped-image" name="avatar">
<form id="form"></form>
</div>
<script>
const MEM_ID="<%= member.id %>";
</script>
<script src="/js/avatar.js"></script>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>
</html>

View file

@ -1,76 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Category list!" }) %>
<body>
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
<div class="container my-3">
<div class="row">
<div class="col-12">
<h2 class="h4 text-white bg-info mb-0 p-4 rounded-top"><%= "Categories" %></h2>
<table class="table table-striped table-bordered table-responsive-lg">
<thead class="thead-light">
<tr>
<th scope="col" class="topic-col">Topic</th>
<th scope="col">Description</th>
<% if (user?.admin){ %> <th scope="col" class="last-post-col">Action</th> <% } %>
</tr>
</thead>
<tbody>
<% categories.forEach(category=>{ %>
<tr>
<td>
<h3 class="h6">
<a href="<%= category.getLink() %>"><%= category.name %></a>
</h3>
</td>
<td>
<div><%= category.desp %></div>
</td>
<% if (user?.admin){ %>
<td>
<a class="btn-danger" onclick="fetch('/api/categories/<%= category.id %>/',{method:'DELETE'})"><i class="bx bx-trash bx-sm"></i></a>
</td>
<% } %>
</tr>
<% }); %>
</tbody>
</table>
</div>
</div>
<% if(typeof page === "number"){ %>
<div class="mb-3 clearfix">
<nav aria-label="Navigate post pages">
<ul class="pagination justify-content-center">
<li class="page-item <%= page > 0 ?"": "disabled" %>">
<a class="page-link" href="/categories?page=<%= page-1 %>" tabindex="-1">Back</a>
</li>
<% for(let i=0; i < pages; i++){ %>
<li class="page-item">
<a class="page-link <%= i==page?'active':'' %>" href="/categories?page=<%= i %>"><%= i+1 %>
<% if (i==page){ %>
<span class="sr-only">(current)</span>
<% } %>
</a>
</li>
<% } %>
<li class="page-item <%= pages-1 > page ?"": "disabled" %>">
<a class="page-link" href="/categories?page=<%= page+1 %>">Next</a>
</li>
</ul>
</nav>
</div>
<% } %>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>
</html>

View file

@ -1,32 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Edit Forum Config!" }) %>
<body style="text-align: center;">
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
<h1>Edit forum config</h1>
<textarea rows="30" cols="75"><%= config %></textarea>
<a onclick="send();" class="btn-primary">Edit config</a>
<script>
const textarea = document.querySelector('textarea');
async function send() {
const res = await fetch('/api/config?text', {
method: 'PUT',
body: textarea.value,
headers: {
'Content-Type': 'application/json'
}
})
if (res.error) return alert(res.error);
alert('Success!');
textarea.value=JSON.stringify( await res.json(),null,4)
}
</script>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>
</html>

View file

@ -1,59 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Create Category!" }) %>
<body>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
<link rel="stylesheet" href="/libs/simplemde/simplemde.min.css">
<script src="/libs/simplemde/simplemde.min.js"></script>
<div class="container my-3">
<div class="col-12">
<h2 class="h4 text-white bg-info mb-3 p-4 rounded">Create new category</h2>
<form class="mb-3">
<div class="form-group">
<label for="topic">Name</label>
<input type="text" class="form-control" id="title" placeholder="Give your category a name" required>
</div>
<div class="form-group">
<label for="comment">Description</label>
<textarea id="textarea"></textarea>
</div>
<button type="submit" class="btn btn-primary">Create</button>
<button type="reset" class="btn btn-danger">Reset</button>
</form>
</div>
</div>
</div>
<script src="/js/editor.js"></script>
<script type="module">
const simplemde = editor("category-create");
import request from "../../js/request.js";
document.addEventListener("submit", async e => {
e.preventDefault();
const response = await request("/api/categories/", "POST", {
name: document.getElementById("title").value,
desp: simplemde.value()
});
simplemde.clearAutosavedValue();
if (response)
window.location.href = "/categories/" + response.id;
});
</script>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>
</html>

View file

@ -1,69 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: "Create thread!" }) %>
<body>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
<link rel="stylesheet" href="/libs/simplemde/simplemde.min.css">
<script src="/libs/simplemde/simplemde.min.js"></script>
<div class="container my-3">
<div class="col-12">
<h2 class="h4 text-white bg-info mb-3 p-4 rounded">Create new thread</h2>
<form class="mb-3">
<div class="form-group">
<label for="topic">Title</label>
<input type="text" class="form-control" id="title" placeholder="Give your thread a title." required>
</div>
<div class="form-group">
<label for="comment">Comment:</label>
<textarea id="textarea"></textarea>
</div>
<div class="form-group">
<label class="form-check-label">
Category:
</label>
<select id="category" class="input">
<% for (const category of categories) { %>
<option value="<%= category.id %>"><%= category.name %></option>
<% } %>
</select>
</div>
<button type="submit" class="btn btn-primary">Create</button>
<button type="reset" class="btn btn-danger">Reset</button>
</form>
</div>
</div>
</div>
<script src="/js/editor.js"></script>
<script type="module">
const simplemde = editor("thread-create");
import request from "../../js/request.js";
document.addEventListener("submit", async e => {
e.preventDefault();
const response = await request("/api/threads/", "POST", {
title: document.getElementById("title").value,
content: simplemde.value(),
category: document.getElementById("category").value
});
if (response) {
simplemde.clearAutosavedValue();
window.location.href = "/threads/" + response.id;
}
});
</script>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>
</html>

View file

@ -1,62 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: member.name }) %>
<body>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
<link rel="stylesheet" href="/libs/simplemde/simplemde.min.css">
<script src="/libs/simplemde/simplemde.min.js"></script>
<div class="container my-3">
<div class="col-12">
<a class="btn-primary" href="/security" >Change your own password</a>
<h2 class="h4 text-white bg-info mb-3 p-4 rounded">Edit User</h2>
<form id="form" class="mb-3">
<input type="text" name="name" placeholder="<%= member.name %>" class="input">
<input type="email" name="email" placeholder="<%= member.email %>" class="input">
<textarea id="textarea" class="input" name="about" rows="4" cols="60" name="content" placeholder="<%=member.about%>"></textarea>
<% if (user?.admin){ %>
Is Admin? <input id='admin' type='checkbox' value='true' name='admin' <%=member.admin ? "checked": ""%>>
<input id='adminHidden' type='hidden' value='false' name='admin'>
<% } %>
Hide Last Seen? <input id='last' type='checkbox' value='true' name='hideLastSeen' <%=member.hideLastSeen ? "checked": ""%>>
<input id='lastHidden' type='hidden' value='false' name='admin'>
<button type="submit" class="btn-primary" style="width:100%;">Update User!</button>
</form>
</div>
</div>
<script src="/js/editor.js"></script>
<script type="module">
import request from "/js/request.js";
const simplemde = editor("user-edit-<%=member.id%>");
document.getElementById("form").addEventListener("submit", async e => {
e.preventDefault();
document.getElementById('adminHidden').disabled = document.getElementById("admin").checked;
document.getElementById('lastHidden').disabled = document.getElementById("last").checked;
const formdata = new FormData(e.target)
const res = await request("/api/users/<%=member.id%>", "PATCH", {
name: formdata.get("name"),
about: simplemde.value(),
admin: formdata.get("admin"),
email: formdata.get("email"),
hideLastSeen: formdata.get("hideLastSeen")
});
simplemde.clearAutosavedValue();
if (res) alert(`User is updated!`);
location.reload();
});
</script>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>
</html>

View file

@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/meta"), {title: type+" error!" }) %>
<body style="text-align: center;">
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/navbar")) %>
<h1 style="color: var(--main);"><%= type %></h1>
<h2 style="color: var(--second);"><%= error %></h2>
<%- include(dataset.getFile(dataset.theme.codename +"/views/extra/footer")) %>
</body>
</html>

View file

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

@ -1,10 +0,0 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%= title || dataset.forum_name %></title>
<meta name="description" content="<%= dataset.description %>">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link 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

@ -1,70 +0,0 @@
<% 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() %></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++)
if (window.location.pathname.includes(menuItems[i].getAttribute("href"))) {
menuItems[i].classList.add("active");
break;
}
</script>
</nav>

View file

@ -1,179 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<%- include("extra/meta", {title: "Welcome to the " + dataset.forum_name }) %>
<body>
<%- include("extra/navbar") %>
<link href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css' rel='stylesheet'>
<div class="container my-3">
<nav class="breadcrumb">
<span class="breadcrumb-item active">
<% if (user) { %>
Welcome, <%= user.name %>
<% } else { %>
Welcome, Guest! <a href="/register">You can press here to register.</a>
<% } %>
</span>
</nav>
<div class="row">
<div class="col-12 col-xl-9">
<div class="category">
<h2 class="h4 text-white bg-danger mb-0 p-4 rounded-top">New threads</h2>
<table class="table table-striped table-bordered table-responsive">
<thead class="thead-light">
<tr>
<th scope="col" class="topic-col">Topic</th>
<th scope="col" class="created-col">Created</th>
<th scope="col">Statistics</th>
<% if (user?.admin){ %> <th scope="col" class="last-post-col">Action</th> <% } %>
</tr>
</thead>
<tbody>
<% newestThreads.forEach(thread=>{ %>
<tr>
<td>
<h3 class="h6">
<% if (thread.deleted) { %> <span class="badge badge-primary">[DELETED]</span><% } %>
<a href="<%= thread.getLink() %>"><%= thread.title %></a>
</h3>
</td>
<td>
<div class="avatar">by <a href="/users/<%= thread.authorID %>"><%= thread.author.name %></a><img src="<%=thread.author.avatar %>"></div>
<div><%= new Date(thread.time).toLocaleString() %></div>
</td>
<td>
<div><%= thread.messages.length %> messages</div>
<div><%= thread.views %> views</div>
</td>
<% if (user?.admin){ %>
<td>
<% if (!thread.deleted){ %>
<a class="btn-danger" onclick="fetch('/api/threads/<%= thread.id %>/',{method:'DELETE'})"><i class="bx bx-trash bx-sm"></i></a>
<% } %>
</td>
<% } %>
</tr>
<% }); %>
</tbody>
</table>
</div>
<div class="category">
<h2 class="h4 text-white bg-danger mb-0 p-4 rounded-top">New messages</h2>
<table class="table table-striped table-bordered table-responsive">
<thead class="thead-light">
<tr>
<th scope="col" class="forum-col">Message</th>
<th scope="col">Date</th>
<th scope="col" class="last-post-col">Author</th>
</tr>
</thead>
<tbody>
<% newestMessages.forEach(message => { %>
<tr>
<td>
<p class="mb-0">
<a href="<%= message.getLink() %>"> <%= message.content.slice(0, 100) %></a>
</p>
</td>
<td>
<div><%= new Date(message.time).toLocaleString() %></div>
</td>
<td>
<div class="avatar">by <a href="<%= message.getLink() %>"><%= message.author.name %></a><img src="<%=message.author.avatar %>"></div>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
</div>
<div class="col-12 col-xl-3">
<aside>
<div class="row">
<div class="col-12 col-sm-6 col-xl-12">
<div class="card mb-3 mb-sm-0 mb-xl-3">
<div class="card-body">
<h2 class="h4 card-title">Members online</h2>
<style>
li.d-inline~li.d-inline::before {
content: ', ';
}
</style>
<ul class="list-unstyled mb-0">
<% onlineMembers.slice(0, 30).forEach(user => { %>
<li class="d-inline">
<a href="/users/<%=user.id %>"><%= user.name %></a>
</li>
<% }); %>
<% if (onlineMemberCount > 30) { %>
<li>
<a href="/users/"><%= onlineMemberCount - 30 %></a> more...
</li>
<% } %>
</ul>
</div>
<div class="card-footer">
<dl class="row mb-0">
<dt class="col-8">Total:</dt>
<dd class="col-4 mb-0"><%= onlineTotal %></dd>
</dl>
<dl class="row mb-0">
<dt class="col-8">Members:</dt>
<dd class="col-4 mb-0"><%= onlineMemberCount %></dd>
</dl>
<dl class="row mb-0">
<dt class="col-8">Guests:</dt>
<dd class="col-4 mb-0"><%= onlineGuests %></dd>
</dl>
</div>
</div>
</div>
<div class="col-12 col-sm-6 col-xl-12">
<div class="card">
<div class="card-body">
<h2 class="h4 card-title">Forum statistics</h2>
<dl class="row mb-0">
<dt class="col-8">Total categories:</dt>
<dd class="col-4 mb-0"><%= categories %></dd>
</dl>
<dl class="row mb-0">
<dt class="col-8">Total threads:</dt>
<dd class="col-4 mb-0"><%= threads %></dd>
</dl>
<dl class="row mb-0">
<dt class="col-8">Total messages:</dt>
<dd class="col-4 mb-0"><%= messages %></dd>
</dl>
<dl class="row mb-0">
<dt class="col-8">Total members:</dt>
<dd class="col-4 mb-0"><%= users %></dd>
</dl>
</div>
<% if(newestMember) {%>
<div class="card-footer">
<div>Newest member:</div>
<div><a href="/users/<%= newestMember.id %>"><%= newestMember.name %></a></div>
</div>
<% }%>
</div>
</div>
</div>
</aside>
</div>
</div>
</div>
<%- include("extra/footer") %>
</body>
</html>

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