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, query, 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) => { const restrictions = config.restrictions.signup; let requiresCode = false; 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, user) => { 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, user) => { res.clearCookie('token'); res.sendStatus(200); })); module.exports = app;