first commit
This commit is contained in:
		
							
								
								
									
										95
									
								
								src/controllers/user.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								src/controllers/user.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										22
									
								
								src/db.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										32
									
								
								src/index.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										32
									
								
								src/middlewares/auth.js
									
									
									
									
									
										Normal 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};
 | 
			
		||||
							
								
								
									
										22
									
								
								src/middlewares/existance.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/middlewares/existance.js
									
									
									
									
									
										Normal 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};
 | 
			
		||||
							
								
								
									
										31
									
								
								src/middlewares/requiredParameters.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/middlewares/requiredParameters.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										18
									
								
								src/routers/api.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										40
									
								
								src/routers/user.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										21
									
								
								src/services/user.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										24
									
								
								src/utils.js
									
									
									
									
									
										Normal 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};
 | 
			
		||||
		Reference in New Issue
	
	Block a user