brainlet/api/v1/users.js

181 lines
5.9 KiB
JavaScript
Raw Normal View History

2020-10-05 20:36:03 +03:00
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
2020-11-20 18:26:56 +02:00
max: 10, // start blocking after 5 requests
2020-10-05 20:36:03 +03:00
message: "You are being rate limited"
});
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 })
], 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;
}
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,
2020-12-03 01:46:29 +02:00
role: startingRole,
color: User.generateColorFromUsername(username)
2020-10-05 20:36:03 +03:00
});
2020-11-21 12:08:32 +02:00
const userObject = await user.getPublicObject();
2020-11-21 12:08:32 +02:00
console.log('[*] [logger] [users] [create] User created', userObject);
2020-10-05 20:36:03 +03:00
res.status(200).json({
error: false,
message: 'SUCCESS_USER_CREATED',
user: userObject
2020-10-05 20:36:03 +03:00
});
} 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) => {
2020-10-05 20:36:03 +03:00
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,
});
}
2020-11-21 12:08:32 +02:00
const userObject = await existingUser.getPublicObject();
2020-11-21 12:08:32 +02:00
console.log('[*] [logger] [users] [token create] Token created', userObject);
2020-10-05 20:36:03 +03:00
res.status(200).json({
error: false,
message: 'SUCCESS_TOKEN_CREATED',
user: userObject,
2020-10-05 20:36:03 +03:00
token
});
});
});
app.get('/current/info', authenticateEndpoint(async (req, res, user) => {
const userObject = await user.getFullObject();
2020-10-05 20:36:03 +03:00
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
2020-10-05 20:36:03 +03:00
},
});
}, 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(),
2020-10-05 20:36:03 +03:00
});
}));
app.post('/browser/token/clear', authenticateEndpoint((req, res, user) => {
res.clearCookie('token');
res.sendStatus(200);
}));
module.exports = app;