const User = require("../../models/User"); const config = require("../../config"); const secret = require("../../secret"); const { authenticateEndpoint } = require("./authfunctions"); // TODO: Might want to use something else (https://blog.benpri.me/blog/2019/01/13/why-you-shouldnt-be-using-bcrypt-and-scrypt/) const bcrypt = require("bcrypt"); const mongoose = require("mongoose"); const { body, param, validationResult } = require("express-validator"); const express = require("express"); const jwt = require("jsonwebtoken"); const app = express.Router(); mongoose.connect(config.mongoUrl, {useNewUrlParser: true, useUnifiedTopology: true}); const rateLimit = require("express-rate-limit"); const createAccountLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour window max: 10, // start blocking after 5 requests message: "You are being rate limited" }); app.get("/account/create/info", async (req, res) => { let requiresCode = false; if (config.restrictions) { const restrictions = config.restrictions.signup; if (restrictions && restrictions.specialCode) { requiresCode = true; } } res.json({ error: false, message: "SUCCESS_ACCOUNT_CREATE_INFO_FETCH", requiresSpecialCode: requiresCode }); }); app.post("/account/create", [ createAccountLimiter, body("username").not().isEmpty().trim().isLength({ min: 3, max: 32 }).isAlphanumeric(), body("email").not().isEmpty().isEmail().normalizeEmail(), body("password").not().isEmpty().isLength({ min: 8, max: 128 }), body("specialCode").optional().isLength({ min: 12, max: 12 }).isAlphanumeric() ], async (req, res) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { res.status(400).json({ error: true, message: "ERROR_REQUEST_INVALID_DATA", errors: errors.array() }); return; } if (config.restrictions) { const restrictions = config.restrictions.signup; if (restrictions && restrictions.specialCode) { const passedSpecialCode = req.body.specialCode; const specialCode = restrictions.specialCode; if (passedSpecialCode && specialCode) { if (specialCode !== passedSpecialCode) { res.status(401).json({ error: true, message: "ERROR_REQUEST_SPECIAL_CODE_MISSING", errors: [{ msg: "No specialCode passed", param: "specialCode", location: "body" }] }); return false; } } else { res.status(401).json({ error: true, message: "ERROR_REQUEST_SPECIAL_CODE_MISSING", errors: [{ msg: "No specialCode passed", param: "specialCode", location: "body" }] }); return false; } } } const username = req.body.username; const existingUser = await User.findByUsername(username); if (existingUser) { res.status(400).json({ error: true, message: "ERROR_REQUEST_USERNAME_EXISTS", errors: [{ value: username, msg: "Username exists", param: "username", location: "body" }] }); return; } const unhashedPassword = req.body.password; const email = req.body.email; const startingRole = "USER"; const hashedPassword = await bcrypt.hash(unhashedPassword, config.bcryptRounds); const user = await User.create({ username, email, password: hashedPassword, role: startingRole, color: User.generateColorFromUsername(username) }); const userObject = await user.getPublicObject(); console.log("[*] [logger] [users] [create] User created", userObject); res.status(200).json({ error: false, message: "SUCCESS_USER_CREATED", user: userObject }); } catch (e) { console.error("Internal server error", e); res.status(500).json({ error: true, message: "INTERNAL_SERVER_ERROR" }); return; } }); app.post("/token/create", [ createAccountLimiter, body("username").not().isEmpty().trim().isAlphanumeric(), body("password").not().isEmpty() ], async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { res.status(400).json({ error: true, message: "ERROR_REQUEST_LOGIN_INVALID" }); return; } const username = req.body.username; const existingUser = await User.findByUsername(username); if (!existingUser) { res.status(403).json({ error: true, message: "ERROR_REQUEST_LOGIN_INVALID" }); return; } const password = req.body.password; let passwordCheck; try { passwordCheck = await bcrypt.compare(password, existingUser.password); } catch(e) { passwordCheck = false; } if (!passwordCheck) { res.status(403).json({ error: true, message: "ERROR_REQUEST_LOGIN_INVALID" }); return; } jwt.sign({ username }, secret.jwtPrivateKey, { expiresIn: "3h" }, async (err, token) => { if (err) { res.status(500).json({ error: true, message: "INTERNAL_SERVER_ERROR" }); return; } // TODO: Ugly fix for setting httponly cookies if (req.body.alsoSetCookie) { res.cookie("token", token, { maxAge: 3 * 60 * 60 * 1000, httpOnly: true, domain: config.address, }); } const userObject = await existingUser.getPublicObject(); console.log("[*] [logger] [users] [token create] Token created", userObject); res.status(200).json({ error: false, message: "SUCCESS_TOKEN_CREATED", user: userObject, token }); }); }); app.get("/current/info", authenticateEndpoint(async (req, res, user) => { const userObject = await user.getPublicObject(); res.status(200).json({ error: false, message: "SUCCESS_USER_DATA_FETCHED", user: { token: req.cookies.token, // TODO: Passing the token like this is *terribly* insecure ...userObject }, }); }, undefined, 0)); app.get("/user/:userid/info", [ param("userid").not().isEmpty().trim().escape().isLength({ min: 24, max: 24 }) ], authenticateEndpoint(async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { res.status(400).json({ error: true, message: "ERROR_REQUEST_INVALID_DATA", errors: errors.array() }); return; } const userid = req.params.userid; if (!userid) { res.sendStatus(400).json({ error: true, message: "ERROR_REQUEST_INVALID_DATA" }); return; } const otherUser = await User.findById(userid); res.status(200).json({ error: false, message: "SUCCESS_USER_DATA_FETCHED", user: await otherUser.getPublicObject(), }); }, undefined, config.roleMap.USER)); app.post("/browser/token/clear", authenticateEndpoint((req, res) => { res.clearCookie("token"); res.sendStatus(200); })); module.exports = app;