Real-time communication with frontend

- Frontend shows heatmap of most visit places
- Maximum accuracy can now be set
- Fix bug where battery chart filtered values wrongly
This commit is contained in:
2020-10-26 23:38:34 +01:00
parent fa60f58d3c
commit e12ed7775b
20 changed files with 770 additions and 161 deletions

View File

@@ -1,21 +1,21 @@
import bodyParser = require('body-parser');
import { bold } from 'chalk';
import * as cors from 'cors';
import { config as dconfig } from 'dotenv';
import * as express from 'express';
import * as figlet from 'figlet';
import * as mongoose from 'mongoose';
import * as cors from 'cors';
import { exit } from 'process';
import * as winston from 'winston';
import { RabbitMQ } from './lib/rabbit';
import { config } from './config';
import { DeleteUser, GetUser, LoginUser, MW_User, PatchUser } from './endpoints/user';
import { GetBeat, GetBeatStats } from './endpoints/beat';
import { GetPhone, PostPhone } from './endpoints/phone';
import { DeleteUser, GetUser, LoginRabbitUser, LoginUser, MW_User, PatchUser, Resource, Topic, VHost } from './endpoints/user';
import { hashPassword, randomPepper, randomString } from './lib/crypto';
import { RabbitMQ } from './lib/rabbit';
import { UserType } from './models/user/user.interface';
import { User } from './models/user/user.model';
import { GetPhone, PostPhone } from './endpoints/phone';
import { GetBeat, GetBeatStats } from './endpoints/beat';
// Load .env
dconfig({ debug: true, encoding: 'UTF-8' });
@@ -107,6 +107,7 @@ async function run() {
await User.create({
name: 'admin',
password: await hashPassword(randomPassword + salt + randomPepper()),
brokerToken: randomString(16),
salt,
createdAt: Date.now(),
lastLogin: 0,
@@ -119,13 +120,6 @@ async function run() {
logger.debug("At least one admin user already exists, skip.");
}
/**
* Message broker
*/
rabbitmq = new RabbitMQ();
await rabbitmq.init();
logger.info("Connected with message broker.");
/**
* HTTP server
*/
@@ -143,14 +137,22 @@ async function run() {
});
app.get('/', (req, res) => res.status(200).send('OK'));
// User authentication
app.post('/user/login', (req, res) => LoginUser(req, res));
app.get('/user/rabbitlogin', (req, res) => LoginRabbitUser(req, res));
app.get('/user/vhost', (req, res) => VHost(req, res));
app.get('/user/resource', (req, res) => Resource(req, res));
app.get('/user/topic', (req, res) => Topic(req, res));
// Basic user actions
app.get('/user/', MW_User, (req, res) => GetUser(req, res));
app.get('/user/:id', MW_User, (req, res) => GetUser(req, res));
app.patch('/user/:id', MW_User, (req, res) => PatchUser(req, res));
app.delete('/user/:id', MW_User, (req, res) => DeleteUser(req, res));
app.post('/user/login', (req, res) => LoginUser(req, res));
app.get('/phone', MW_User, (req, res) => GetPhone(req, res));
app.get('/phone/:id', MW_User, (req, res) => GetPhone(req, res));
app.get('/phone', MW_User, (req, res) => GetPhone(req, res));
app.post('/phone', MW_User, (req, res) => PostPhone(req, res));
app.get('/beat/', MW_User, (req, res) => GetBeat(req, res));
@@ -159,6 +161,13 @@ async function run() {
app.listen(config.http.port, config.http.host, () => {
logger.info(`HTTP server is running at ${config.http.host}:${config.http.port}`);
});
/**
* Message broker
*/
rabbitmq = new RabbitMQ();
await rabbitmq.init();
logger.info("Connected with message broker.");
}
run();

View File

@@ -35,7 +35,7 @@ export async function GetBeat(req: LivebeatRequest, res: Response) {
$gte: new Date((from | 0) * 1000),
$lte: new Date((to | Date.now() /1000) * 1000)
}
}).sort({ _id: -1 });
}).sort({ _id: 1 });
res.status(200).send(beats);
} else {
res.status(404).send({ message: 'Phone not found' });

View File

@@ -2,10 +2,11 @@ import { Request, Response } from "express";
import { verifyPassword } from "../lib/crypto";
import { User } from "../models/user/user.model";
import { sign, decode, verify } from 'jsonwebtoken';
import { JWT_SECRET, logger } from "../app";
import { JWT_SECRET, logger, RABBITMQ_URI } from "../app";
import { LivebeatRequest } from '../lib/request';
import { SchemaTypes } from "mongoose";
import { Phone } from "../models/phone/phone.model";
import { UserType } from "../models/user/user.interface";
export async function GetUser(req: LivebeatRequest, res: Response) {
let user: any = req.user;
@@ -64,6 +65,130 @@ export async function LoginUser(req: Request, res: Response) {
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;
if (username === undefined || password === undefined) {
res.status(200).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.status(200).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.status(200).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.status(200).send('allow administrator');
} else {
// Not an admin, grant user privilieges
res.status(200).send('allow user')
}
return;
}
res.status(200).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.

View File

@@ -30,9 +30,9 @@ export class RabbitMQ {
return;
}
logger.info(`New beat from ${phone.displayName} with ${msg.gpsLocation[2]} accuracy and ${msg.battery}% battery`)
logger.info(`New beat from ${phone.displayName} with ${msg.gpsLocation[2]} accuracy and ${msg.battery}% battery`);
Beat.create({
const newBeat = await Beat.create({
phone: phone._id,
coordinate: [msg.gpsLocation[0], msg.gpsLocation[1]],
accuracy: msg.gpsLocation[2],
@@ -40,6 +40,8 @@ export class RabbitMQ {
battery: msg.battery,
createdAt: msg.timestamp
});
this.channel!.publish('amq.topic', '.', Buffer.from(JSON.stringify(newBeat.toJSON())));
}, { noAck: true });
}

View File

@@ -10,7 +10,8 @@ const schemaBeat = new Schema({
createdAt: { type: SchemaTypes.Date, required: false }
}, {
timestamps: {
createdAt: true
createdAt: true,
updatedAt: false
},
versionKey: false
});

View File

@@ -13,5 +13,6 @@ export interface IUser extends Document {
type: UserType,
lastLogin: Date,
twoFASecret?: string,
brokerToken: string,
createdAt?: Date
}

View File

@@ -6,6 +6,7 @@ const schemaUser = new Schema({
salt: { type: String, required: true },
type: { type: String, required: true, default: 'user' }, // This could be user, admin, guest
twoFASecret: { type: String, required: false },
brokerToken: { type: String, required: true },
lastLogin: { type: Date, required: true, default: Date.now },
}, {
timestamps: {