first commit

This commit is contained in:
2025-01-23 01:12:17 +03:00
parent 651afd6e36
commit 1b09267e47
27 changed files with 4274 additions and 0 deletions

95
src/controllers/user.js Normal file
View File

@@ -0,0 +1,95 @@
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';
import path from 'path';
import { Jimp } from 'jimp';
import fs from 'fs';
import UserService from "../services/user.js";
import utils from '../utils.js';
dotenv.config({path: ".env"});
class UserController {
async register(req, res) {
const {username, password, passwordConfirm} = req.body;
if (password != passwordConfirm) return res.status(400).send("Passwords do not match");
let hashedPassword = await bcrypt.hash(password, 8);
await UserService.register(username, hashedPassword);
if (process.env.REQUIRE_TOKEN == "true" && process.env.DELETE_TOKEN_ON_USE == "true") {
utils.removeFromFile('./inviteTokens.txt', req.body.inviteToken);
}
req.session.jwt = jwt.sign({ username }, process.env.SECRET, {expiresIn: "1y"});
return res.redirect("/index");
}
async login(req, res) {
const {username, password} = req.body;
const storedPassword = await UserService.getPassword(username);
if (!bcrypt.compareSync(password, storedPassword)) {
return res.status(403).send("Password is not correct");
}
req.session.jwt = jwt.sign({ username }, process.env.SECRET, {expiresIn: "1y"});
return res.redirect("/index");
}
async logout(req, res) {
req.session.destroy();
return res.redirect("/login");
}
async uploadSkin(req, res) {
const token = req.session.jwt;
const decoded = jwt.decode(token);
const tempPath = req.file.path;
const targetPath = `/opt/skins/${decoded.username}.png`;
if (path.extname(req.file.originalname).toLowerCase() !== ".png") {
return res.status(400).send("Only .png files are allowed!");
}
const image = await Jimp.read(tempPath);
if (image.bitmap.width != 64 || image.bitmap.height != 64) {
fs.unlinkSync(targetPath);
return res.status(400).send('This does not look like a minecraft skin.');
}
fs.renameSync(tempPath, targetPath, err => {
if (err) return res.status(500).send("Ooops! Something went wrong! Please, report to the developer.");
});
return res.status(200).send("Skin uploaded!");
}
async uploadCape(req, res) {
const token = req.session.jwt;
const decoded = jwt.decode(token);
const tempPath = req.file.path;
const targetPath = `/opt/cloaks/${decoded.username}.png`;
if (path.extname(req.file.originalname).toLowerCase() !== ".png") {
return res.status(400).send("Only .png files are allowed!");
}
const image = await Jimp.read(tempPath);
if ((image.bitmap.width != 64 || image.bitmap.height != 32) && (image.bitmap.width != 128 || image.bitmap.height != 64)) {
fs.unlinkSync(tempPath);
return res.status(400).send('This does not look like a minecraft cape.');
}
fs.renameSync(tempPath, targetPath, err => {
if (err) return res.status(500).send("Ooops! Something went wrong! Please, report to the developer.");
});
return res.status(200).send("Cape uploaded!");
}
}
export default new UserController();

22
src/db.js Normal file
View File

@@ -0,0 +1,22 @@
import pg from 'pg';
import fs from 'fs';
import dotenv from 'dotenv';
dotenv.config({path: ".env"});
const { Pool } = pg;
console.log("Connecting to PostgreSQL database");
const pool = new Pool({
user: process.env.DBUSER,
host: process.env.DBHOST,
database: process.env.DBNAME,
password: process.env.DBPASS,
port: process.env.DBPORT
});
pool.query(fs.readFileSync('./db_schema.psql').toString());
export default pool;

32
src/index.js Normal file
View File

@@ -0,0 +1,32 @@
import express from 'express';
import dotenv from 'dotenv';
import session from 'express-session';
import cookieParser from 'cookie-parser';
import path from 'path';
import ApiRouter from './routers/api.js';
import UserRouter from './routers/user.js';
const app = express();
dotenv.config({path: ".env"});
app.use(session({
secret: process.env.SECRET,
resave: true,
saveUninitialized: false,
cookie: { maxAge: 1000 * 60 * 60 * 24 }
}));
app.use(express.static(path.join('./public')));
app.use(express.urlencoded({extended: false}));
app.use(express.json());
app.use(cookieParser());
app.set('view engine', 'pug');
app.use('/api', ApiRouter);
app.use('/', UserRouter);
app.listen(process.env.PORT, () => {
console.log("App has been started!");
});

32
src/middlewares/auth.js Normal file
View File

@@ -0,0 +1,32 @@
import fs from 'fs';
import dotenv from 'dotenv';
import jwt from 'jsonwebtoken';
dotenv.config({path: ".env"});
const authenticate = async (req, res, next) => {
const token = req.session.jwt;
if (!token || !jwt.verify(token, process.env.SECRET)) {
req.session.destroy();
return res.redirect("/login");
}
next();
};
const validateInviteToken = async (req, res, next) => {
if (process.env.REQUIRE_TOKEN != "true") return next();
const { inviteToken } = req.body;
fs.appendFileSync('./inviteTokens.txt', '');
const inviteTokens = fs.readFileSync('./inviteTokens.txt').toString().split('\n');
let tokenValid = false;
inviteTokens.forEach((token) => {
if (token == inviteToken) tokenValid = true;
});
if (!tokenValid) return res.status(400).send("Token is not valid");
next();
};
export default {authenticate, validateInviteToken};

View File

@@ -0,0 +1,22 @@
import UserService from '../services/user.js';
const userDoesNotExist = async (req, res, next) => {
const { username } = req.body;
if (await UserService.exists(username)) {
return res.status(401).send("Such user exists!");
}
next();
};
const userExist = async (req, res, next) => {
const { username } = req.body;
if (!(await UserService.exists(username))) {
return res.status(401).send("Such user does not exists!");
}
next();
}
export default {userDoesNotExist, userExist};

View File

@@ -0,0 +1,31 @@
import dotenv from 'dotenv';
dotenv.config({path: ".env"});
const requireUsername = async (req, res, next) => {
const { username } = req.body;
if (!username) return res.status(400).send("Username is requires");
next();
};
const requirePassword = async (req, res, next) => {
const { password } = req.body;
if (!password) return res.status(400).send("Password is required");
next();
};
const requireInviteToken = async (req, res, next) => {
const { inviteToken } = req.body;
if (!inviteToken && process.env.REQUIRE_TOKEN == "true") return res.status(400).send("Invite token is required");
next();
};
const requireFile = async (req, res, next) => {
if (!req.file) return res.status(400).send("Please, select a file!");
next();
}
export default {requireUsername, requirePassword, requireInviteToken, requireFile};

18
src/routers/api.js Normal file
View File

@@ -0,0 +1,18 @@
import { Router } from 'express';
import requiredParameters from '../middlewares/requiredParameters.js';
import existance from '../middlewares/existance.js';
import auth from '../middlewares/auth.js';
import utils from '../utils.js';
import UserController from '../controllers/user.js';
const ApiRouter = new Router();
ApiRouter.post('/register', requiredParameters.requireUsername, requiredParameters.requirePassword, auth.validateInviteToken, existance.userDoesNotExist, UserController.register);
ApiRouter.post('/login', requiredParameters.requireUsername, requiredParameters.requirePassword, existance.userExist, UserController.login);
ApiRouter.get('/logout', auth.authenticate, UserController.logout);
ApiRouter.post('/uploadSkin', auth.authenticate, utils.upload.single('file'), requiredParameters.requireFile, UserController.uploadSkin);
ApiRouter.post('/uploadCape', auth.authenticate, utils.upload.single('file'), requiredParameters.requireFile, UserController.uploadCape);
export default ApiRouter;

40
src/routers/user.js Normal file
View File

@@ -0,0 +1,40 @@
import { Router } from 'express';
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';
import auth from '../middlewares/auth.js';
import UserService from '../services/user.js';
dotenv.config({path: ".env"});
const UserRouter = new Router();
UserRouter.get('/register', async (req, res) => {
if (req.session.jwt && jwt.verify(req.session.jwt, process.env.SECRET))
return res.redirect("/index");
return res.render("register.pug", {
require_token: process.env.REQUIRE_TOKEN == "true"? true : false
});
});
UserRouter.get(['/', '/login'], async (req, res) => {
if(req.session.jwt && jwt.verify(req.session.jwt, process.env.SECRET))
return res.redirect("/index");
return res.render("login.pug");
});
UserRouter.get('/index', auth.authenticate, async (req, res) => {
if (!req.session.jwt || !jwt.verify(req.session.jwt, process.env.SECRET))
return res.redirect("/login");
const decoded = jwt.decode(req.session.jwt);
return res.render('index.pug', {
username: decoded.username,
can_have_cloak: await UserService.canHaveCloak(decoded.username)
});
})
export default UserRouter;

21
src/services/user.js Normal file
View File

@@ -0,0 +1,21 @@
import db from '../db.js';
class UserService {
async register(username, password) {
await db.query("INSERT INTO users (username, password) VALUES ($1, $2)", [username, password]);
}
async exists(username) {
return (await db.query("SELECT * FROM users WHERE username = $1", [username])).rowCount > 0;
}
async getPassword(username) {
return (await db.query("SELECT password FROM users WHERE username = $1", [username])).rows[0].password;
}
async canHaveCloak(username) {
return (await db.query("SELECT can_have_cloak FROM users WHERE username = $1", [username])).rows[0].can_have_cloak;
}
};
export default new UserService();

24
src/utils.js Normal file
View File

@@ -0,0 +1,24 @@
import multer from "multer";
import fs from 'fs';
const upload = multer({
dest: "./temp",
fileSize: 3072
});
const removeFromFile = (filename, toRemove) => {
let content = fs.readFileSync(filename).toString();
let dataArray = content.split('\n');
let lastIndex = -1;
for (let i = 0; i < dataArray.length; i ++) {
if (dataArray[i].includes(toRemove)) {
lastIndex = i;
break;
}
}
dataArray.splice(lastIndex, 1);
const updatedData = dataArray.join('\n');
fs.writeFileSync(filename, updatedData);
}
export default {upload, removeFromFile};