282 lines
8.4 KiB
TypeScript
282 lines
8.4 KiB
TypeScript
import { Request, Response } from 'express';
|
|
import { decode, sign, verify } from 'jsonwebtoken';
|
|
|
|
import { JWT_SECRET, logger, RABBITMQ_URI } from '../app';
|
|
import * as jwt from 'jsonwebtoken';
|
|
import { config } from '../config';
|
|
import { hashPassword, randomPepper, randomString, verifyPassword } from '../lib/crypto';
|
|
import { LivebeatRequest } from '../lib/request';
|
|
import { UserType } from '../models/user/user.interface';
|
|
import { User } from '../models/user/user.model';
|
|
|
|
export async function GetUser(req: LivebeatRequest, res: Response) {
|
|
let user: any = req.params.id === undefined ? req.user : await User.findById(req.params.id);
|
|
|
|
if (user === null) {
|
|
res.status(404).send();
|
|
return;
|
|
}
|
|
|
|
user.password = undefined;
|
|
user.salt = undefined;
|
|
user.__v = undefined;
|
|
|
|
res.status(200).send(user);
|
|
}
|
|
|
|
export async function PostUser(req: LivebeatRequest, res: Response) {
|
|
// Only admin can create new users
|
|
if (req.user?.type !== UserType.ADMIN) {
|
|
res.status(401).send({ message: 'Only admins can create new users.' });
|
|
return;
|
|
}
|
|
|
|
const name = req.body.name;
|
|
const password = req.body.password;
|
|
const type = req.body.type;
|
|
|
|
if (name === undefined || password === undefined || type === undefined) {
|
|
res.status(400).send();
|
|
return;
|
|
}
|
|
|
|
if (!Object.values(UserType).includes(type)) {
|
|
res.status(400).send({ message: 'The user type can only be \'guest\', \'user\', \'admin\'.' });
|
|
return;
|
|
}
|
|
|
|
if (await User.countDocuments({ name }) === 1) {
|
|
res.status(409).send();
|
|
return;
|
|
}
|
|
|
|
const salt = randomString(config.authentification.salt_length);
|
|
const brokerToken = randomString(16);
|
|
const hashedPassword = await hashPassword(password + salt + randomPepper()).catch(error => {
|
|
res.status(400).send({ message: 'Provided password is too weak and cannot be used.' });
|
|
return;
|
|
}) as string;
|
|
|
|
const newUser = await User.create({
|
|
name,
|
|
password: hashedPassword,
|
|
salt,
|
|
brokerToken,
|
|
type,
|
|
lastLogin: new Date(0)
|
|
});
|
|
|
|
// Create setup token that the new user can use to change his password.
|
|
const setupToken = jwt.sign({ setupForUser: newUser._id }, JWT_SECRET, { expiresIn: '1d' });
|
|
|
|
res.status(200).send({ setupToken });
|
|
}
|
|
|
|
export async function DeleteUser(req: Request, res: Response) {
|
|
|
|
}
|
|
|
|
export async function PatchUser(req: Request, res: Response) {
|
|
|
|
}
|
|
|
|
export async function LoginUser(req: Request, res: Response) {
|
|
const username = req.body.username;
|
|
const password = req.body.password;
|
|
const twoFA = req.body.twoFA;
|
|
|
|
const user = await User.findOne({ name: username });
|
|
|
|
// Check if user exists
|
|
if (user == undefined) {
|
|
setTimeout(() => {
|
|
res.status(404).send({ message: "Either the username or password is wrong." });
|
|
}, Math.random() * 1500 + 400);
|
|
return;
|
|
}
|
|
|
|
// Check if 2FA is turned on (the attack doesn't know yet if the password is wrong)
|
|
if (user.twoFASecret != undefined) {
|
|
if (twoFA == undefined) {
|
|
res.status(401).send({ message: "2FA code is required." });
|
|
return;
|
|
}
|
|
// TODO: Implement 2FA logic here
|
|
}
|
|
|
|
// Check if password is wrong
|
|
if (!await verifyPassword(password + user.salt, user.password)) {
|
|
res.status(404).send({ message: 'Either the username or password is wrong.' });
|
|
return;
|
|
}
|
|
|
|
// We're good. Create JWT token.
|
|
const token = sign({ user: user._id }, JWT_SECRET, { expiresIn: '30d' });
|
|
|
|
user.lastLogin = new Date(Date.now());
|
|
await user.save();
|
|
|
|
logger.info(`User ${user.name} logged in.`)
|
|
res.status(200).send({ token });
|
|
}
|
|
|
|
/**
|
|
* This function handles all logins to RabbitMQ since they need a differnt type of response
|
|
* then requests from frontends (web and phone).
|
|
*/
|
|
export async function LoginRabbitUser(req: Request, res: Response) {
|
|
const username = req.query.username;
|
|
const password = req.query.password;
|
|
res.status(200);
|
|
|
|
if (username === undefined || password === undefined) {
|
|
res.send('deny');
|
|
return;
|
|
}
|
|
|
|
// Check if request comes from backend. Basicly, we permitting ourself to connect with RabbitMQ.
|
|
if (username === "backend" && password === RABBITMQ_URI.split(':')[2].split('@')[0]) {
|
|
res.send('allow administrator');
|
|
return;
|
|
}
|
|
|
|
// Get user from database
|
|
const user = await User.findOne({ name: username.toString() });
|
|
|
|
// If we are here, it means we have a non-admin user.
|
|
if (user === null) {
|
|
res.send('deny');
|
|
return;
|
|
}
|
|
|
|
// Auth token for message broker is stored in plain text since it's randomly generated and only grants access to the broker.
|
|
if (user.brokerToken === password.toString()) {
|
|
if (user.type === UserType.ADMIN) {
|
|
res.send('allow administrator');
|
|
} else {
|
|
// Not an admin, grant user privilieges
|
|
res.send('allow user')
|
|
}
|
|
return;
|
|
}
|
|
|
|
res.send('deny');
|
|
}
|
|
|
|
/**
|
|
* This function basicly allows access to the root vhost if the user is known.
|
|
*/
|
|
export async function VHost(req: Request, res: Response) {
|
|
const vhost = req.query.vhost;
|
|
const username = req.query.username;
|
|
|
|
if (vhost === undefined || username === undefined) {
|
|
res.status(200).send('deny');
|
|
return;
|
|
}
|
|
|
|
if (vhost != '/') {
|
|
res.status(200).send('deny');
|
|
return;
|
|
}
|
|
|
|
// Check if user is us
|
|
if (username === 'backend') {
|
|
res.status(200).send('allow');
|
|
return;
|
|
}
|
|
|
|
const user = await User.findOne({ name: username.toString() });
|
|
if (user === null) {
|
|
// Deny if user doesn't exist.
|
|
res.status(200).send('deny');
|
|
} else {
|
|
res.status(200).send('allow');
|
|
}
|
|
}
|
|
|
|
export async function Resource(req: Request, res: Response) {
|
|
const username = req.query.username;
|
|
const vhost = req.query.vhost;
|
|
const resource = req.query.resource;
|
|
const name = req.query.name;
|
|
const permission = req.query.permission;
|
|
const tags = req.query.tags;
|
|
|
|
if (username === undefined || vhost === undefined || resource === undefined || name === undefined || permission === undefined || tags === undefined) {
|
|
res.status(200).send('deny');
|
|
return;
|
|
}
|
|
|
|
// Check if it's us
|
|
if (username.toString() == 'backend') {
|
|
res.status(200).send('allow');
|
|
return;
|
|
}
|
|
|
|
// Deny if not root vhost
|
|
if (vhost.toString() != '/') {
|
|
res.status(200).send('deny');
|
|
return;
|
|
}
|
|
|
|
// Check if user exists
|
|
const user = await User.findOne({ name: username.toString() });
|
|
if (user == null) {
|
|
res.status(200).send('deny');
|
|
return;
|
|
}
|
|
|
|
if (tags.toString() == "administrator" && user.type != UserType.ADMIN) {
|
|
res.status(200).send('deny');
|
|
return;
|
|
}
|
|
|
|
// TODO: This has to change if we want to allow users to see the realtime movement of others.
|
|
if (resource.toString().startsWith('tracker-') && resource != 'tracker-' + username) {
|
|
res.status(200).send('deny');
|
|
return;
|
|
}
|
|
|
|
res.status(200).send('allow');
|
|
}
|
|
|
|
export async function Topic(req: Request, res: Response) {
|
|
res.status(200).send('allow');
|
|
}
|
|
|
|
/**
|
|
* This middleware validates any tokens that are required to access most of the endpoints.
|
|
* Note: This validation doesn't contain any permission checking.
|
|
*/
|
|
export async function MW_User(req: LivebeatRequest, res: Response, next: () => void) {
|
|
if (req.headers.token === undefined) {
|
|
res.status(401).send({ message: "Token not specified" });
|
|
return;
|
|
}
|
|
const token = req.headers.token.toString();
|
|
|
|
try {
|
|
// Verify token
|
|
if(await verify(token, JWT_SECRET, { algorithms: ['HS256'] })) {
|
|
// Token is valid, now look if user is in db (in case he got deleted)
|
|
const id = decode(token, { json: true })!.user;
|
|
const db = await User.findById(id);
|
|
|
|
if (db !== undefined && db !== null) {
|
|
req.user = db
|
|
next();
|
|
return;
|
|
} else {
|
|
res.status(401).send({ message: "Token is not valid" });
|
|
}
|
|
} else {
|
|
res.status(401).send({ message: "Token is not valid" });
|
|
}
|
|
} catch (err) {
|
|
if (err) {
|
|
res.status(500).send({ message: "We failed validating your token for some reason." });
|
|
logger.error(err);
|
|
}
|
|
}
|
|
} |