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:
@@ -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();
|
||||
@@ -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' });
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ const schemaBeat = new Schema({
|
||||
createdAt: { type: SchemaTypes.Date, required: false }
|
||||
}, {
|
||||
timestamps: {
|
||||
createdAt: true
|
||||
createdAt: true,
|
||||
updatedAt: false
|
||||
},
|
||||
versionKey: false
|
||||
});
|
||||
|
||||
@@ -13,5 +13,6 @@ export interface IUser extends Document {
|
||||
type: UserType,
|
||||
lastLogin: Date,
|
||||
twoFASecret?: string,
|
||||
brokerToken: string,
|
||||
createdAt?: Date
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user