Livebeat is now able to send, store and show beats
This commit is contained in:
@@ -4,6 +4,7 @@ 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';
|
||||
@@ -13,6 +14,8 @@ import { DeleteUser, GetUser, LoginUser, MW_User, PatchUser } from './endpoints/
|
||||
import { hashPassword, randomPepper, randomString } from './lib/crypto';
|
||||
import { UserType } from './models/user/user.interface';
|
||||
import { User } from './models/user/user.model';
|
||||
import { GetPhone, PostPhone } from './endpoints/phone';
|
||||
import { GetBeat } from './endpoints/beat';
|
||||
|
||||
// Load .env
|
||||
dconfig({ debug: true, encoding: 'UTF-8' });
|
||||
@@ -127,15 +130,29 @@ async function run() {
|
||||
*/
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json({ limit: '5kb' }));
|
||||
|
||||
app.use((req, res, next) => {
|
||||
res.on('finish', () => {
|
||||
const done = Date.now();
|
||||
logger.debug(`${req.method} - ${req.url} ${JSON.stringify(req.body)} -> ${res.statusCode}`);
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
app.get('/', (req, res) => res.status(200).send('OK'));
|
||||
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/:id', (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));
|
||||
|
||||
app.listen(config.http.port, config.http.host, () => {
|
||||
logger.info(`HTTP server is running at ${config.http.host}:${config.http.port}`);
|
||||
});
|
||||
|
||||
30
backend/endpoints/beat.ts
Normal file
30
backend/endpoints/beat.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Response } from "express";
|
||||
import { logger } from "../app";
|
||||
import { LivebeatRequest } from "../lib/request";
|
||||
import { Beat } from "../models/beat/beat.model.";
|
||||
import { Phone } from "../models/phone/phone.model";
|
||||
|
||||
export interface IFilter {
|
||||
phone: string,
|
||||
time: {
|
||||
from: number,
|
||||
to: number
|
||||
},
|
||||
max: number
|
||||
}
|
||||
|
||||
export async function GetBeat(req: LivebeatRequest, res: Response) {
|
||||
const filter: IFilter = req.body.filter as IFilter;
|
||||
|
||||
// If no filters are specified, we return the last 500 points. We take the first phone as default.
|
||||
if (filter === undefined) {
|
||||
const phone = await Phone.findOne({ user: req.user?._id });
|
||||
logger.debug(`No filters were provided! Take ${phone?.displayName} as default.`);
|
||||
|
||||
if (phone !== undefined && phone !== null) {
|
||||
logger.debug("Query for latest beats ...");
|
||||
const beats = await Beat.find({ phone: phone._id }).limit(800).sort({ _id: -1 });
|
||||
res.status(200).send(beats);
|
||||
}
|
||||
}
|
||||
}
|
||||
63
backend/endpoints/phone.ts
Normal file
63
backend/endpoints/phone.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Response } from "express";
|
||||
import { logger } from "../app";
|
||||
import { LivebeatRequest } from "../lib/request";
|
||||
import { Phone } from "../models/phone/phone.model";
|
||||
|
||||
export async function GetPhone(req: LivebeatRequest, res: Response) {
|
||||
const phoneId: String = req.params['id'];
|
||||
|
||||
if (phoneId === undefined) {
|
||||
res.status(400).send();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check database for phone
|
||||
const phone = await Phone.findOne({ androidId: phoneId, user: req.user?._id });
|
||||
if (phone === undefined) {
|
||||
res.status(404).send();
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).send(phone);
|
||||
}
|
||||
|
||||
export async function PostPhone(req: LivebeatRequest, res: Response) {
|
||||
const androidId: String = req.body.androidId;
|
||||
const modelName: String = req.body.modelName;
|
||||
const displayName: String = req.body.displayName;
|
||||
const operatingSystem: String = req.body.operatingSystem;
|
||||
const architecture: String = req.body.architecture;
|
||||
|
||||
if (androidId === undefined ||
|
||||
modelName === undefined ||
|
||||
displayName === undefined ||
|
||||
operatingSystem === undefined ||
|
||||
architecture === undefined) {
|
||||
logger.debug("Request to /phone failed because of missing parameters.");
|
||||
res.status(400).send();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if phone already exists
|
||||
const phone = await Phone.findOne({ androidId, user: req.user?._id });
|
||||
|
||||
if (phone !== null) {
|
||||
logger.debug("Request to /phone failed because phone already exists.");
|
||||
res.status(409).send();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create phone
|
||||
await Phone.create({
|
||||
androidId,
|
||||
displayName,
|
||||
modelName,
|
||||
operatingSystem,
|
||||
architecture,
|
||||
user: req.user?._id
|
||||
});
|
||||
|
||||
logger.info(`New device (${displayName}) registered for ${req.user?.name}.`)
|
||||
|
||||
res.status(200).send();
|
||||
}
|
||||
@@ -3,7 +3,8 @@ 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 { IUser } from "../models/user/user.interface";
|
||||
import { LivebeatRequest } from '../lib/request';
|
||||
import { SchemaTypes } from "mongoose";
|
||||
|
||||
export async function GetUser(req: Request, res: Response) {
|
||||
|
||||
@@ -48,7 +49,7 @@ export async function LoginUser(req: Request, res: Response) {
|
||||
}
|
||||
|
||||
// We're good. Create JWT token.
|
||||
const token = sign({ user: user._id }, JWT_SECRET!, { notBefore: Date.now(), expiresIn: '30d' });
|
||||
const token = sign({ user: user._id }, JWT_SECRET, { expiresIn: '30d' });
|
||||
|
||||
logger.info(`User ${user.name} logged in.`)
|
||||
res.status(200).send({ token });
|
||||
@@ -58,29 +59,34 @@ export async function LoginUser(req: Request, res: Response) {
|
||||
* 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: Request, res: Response, next: () => void) {
|
||||
export async function MW_User(req: LivebeatRequest, res: Response, next: () => void) {
|
||||
if (req.headers.token === undefined) {
|
||||
res.status(401).send();
|
||||
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'] })) {
|
||||
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: number = Number(decode(token, { json: true })!.id);
|
||||
const db = await User.findOne({ where: { id } });
|
||||
if (db !== undefined) {
|
||||
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();
|
||||
res.status(401).send({ message: "Token is not valid" });
|
||||
}
|
||||
} else {
|
||||
res.status(401).send();
|
||||
res.status(401).send({ message: "Token is not valid" });
|
||||
}
|
||||
} catch (err) {
|
||||
if (err) res.status(401).send();
|
||||
if (err) {
|
||||
res.status(500).send({ message: "We failed validating your token for some reason." });
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
import * as amqp from 'amqplib';
|
||||
import { logger, RABBITMQ_URI } from '../app';
|
||||
import { Beat } from '../models/beat/beat.model.';
|
||||
import { Phone } from '../models/phone/phone.model';
|
||||
|
||||
interface IBeat {
|
||||
token: string,
|
||||
gpsLocation: Array<number>,
|
||||
battery: number,
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export class RabbitMQ {
|
||||
connection: amqp.Connection | null = null;
|
||||
@@ -9,12 +18,32 @@ export class RabbitMQ {
|
||||
this.connection = await amqp.connect(RABBITMQ_URI);
|
||||
this.channel = await this.connection.createChannel();
|
||||
|
||||
this.channel.consume('Tracker', (msg) => {
|
||||
logger.debug("Received from broker: " + msg?.content.toString());
|
||||
}, { noAck: false });
|
||||
this.channel.consume('tracker', async (income) => {
|
||||
if (income === undefined || income === null) return;
|
||||
|
||||
const msg: IBeat = JSON.parse(income.content.toString()) as IBeat
|
||||
|
||||
// Get phone
|
||||
const phone = await Phone.findOne({ androidId: msg.token });
|
||||
if (phone == undefined) {
|
||||
logger.info(`Received beat from unknown device with id ${msg.token}`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`New beat from ${phone.displayName} with ${msg.gpsLocation[2]} accuracy and ${msg.battery}% battery`)
|
||||
|
||||
Beat.create({
|
||||
phone: phone._id,
|
||||
coordinate: [msg.gpsLocation[0], msg.gpsLocation[1]],
|
||||
accuracy: msg.gpsLocation[2],
|
||||
speed: msg.gpsLocation[3],
|
||||
battery: msg.battery,
|
||||
createdAt: msg.timestamp
|
||||
});
|
||||
}, { noAck: true });
|
||||
}
|
||||
|
||||
async publish(queueName = 'Tracker', data: any) {
|
||||
async publish(queueName = 'tracker', data: any) {
|
||||
if (this.connection == undefined) await this.init()
|
||||
this.channel?.sendToQueue(queueName, Buffer.from(data));
|
||||
}
|
||||
|
||||
6
backend/lib/request.ts
Normal file
6
backend/lib/request.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Request } from "express";
|
||||
import { IUser } from "../models/user/user.interface";
|
||||
|
||||
export interface LivebeatRequest extends Request {
|
||||
user?: IUser
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Document } from 'mongoose';
|
||||
import { IPhone } from '../phone/phone.interface';
|
||||
|
||||
export interface ITrack extends Document {
|
||||
export interface IBeat extends Document {
|
||||
coordinate?: number[],
|
||||
velocity?: number,
|
||||
accuracy: number,
|
||||
speed: number,
|
||||
battery?: number,
|
||||
magneticField?: number,
|
||||
phone: IPhone,
|
||||
createdAt?: Date
|
||||
}
|
||||
6
backend/models/beat/beat.model..ts
Normal file
6
backend/models/beat/beat.model..ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Model, model } from 'mongoose';
|
||||
import { IBeat } from './beat.interface';
|
||||
import { schemaBeat } from './beat.schema';
|
||||
|
||||
const modelBeat: Model<IBeat> = model<IBeat>('Beat', schemaBeat, 'Beat');
|
||||
export { modelBeat as Beat };
|
||||
16
backend/models/beat/beat.schema.ts
Normal file
16
backend/models/beat/beat.schema.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Schema, SchemaTypes } from 'mongoose';
|
||||
import { Phone } from '../phone/phone.model';
|
||||
|
||||
const schemaBeat = new Schema({
|
||||
coordinate: { type: [Number], required: false },
|
||||
accuracy: { type: Number, required: false },
|
||||
speed: { type: Number, required: false },
|
||||
battery: { type: Number, required: false },
|
||||
phone: { type: SchemaTypes.ObjectId, required: true, default: 'user' }
|
||||
}, {
|
||||
timestamps: {
|
||||
createdAt: true
|
||||
}
|
||||
});
|
||||
|
||||
export { schemaBeat };
|
||||
@@ -2,6 +2,7 @@ import { Document } from 'mongoose';
|
||||
import { IUser } from '../user/user.interface';
|
||||
|
||||
export interface IPhone extends Document {
|
||||
androidId: String,
|
||||
displayName: String,
|
||||
modelName: String,
|
||||
operatingSystem: String,
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Schema } from 'mongoose';
|
||||
import { Mongoose, Schema, SchemaType, SchemaTypes } from 'mongoose';
|
||||
import { User } from '../user/user.model';
|
||||
|
||||
const schemaPhone = new Schema({
|
||||
androidId: { type: String, required: true },
|
||||
displayName: { type: String, required: true },
|
||||
modelName: { type: String, required: false },
|
||||
operatingSystem: { type: String, required: false },
|
||||
architecture: { type: String, required: false },
|
||||
user: { type: User, required: true }
|
||||
user: { type: SchemaTypes.ObjectId, required: true }
|
||||
}, {
|
||||
timestamps: {
|
||||
createdAt: true,
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { Model, model } from 'mongoose';
|
||||
import { ITrack } from './track.interface';
|
||||
import { schemaTrack } from './track.schema';
|
||||
|
||||
const modelTrack: Model<ITrack> = model<ITrack>('Track', schemaTrack, 'Track');
|
||||
export { modelTrack as Phone };
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Schema } from 'mongoose';
|
||||
import { Phone } from '../phone/phone.model';
|
||||
|
||||
const schemaTrack = new Schema({
|
||||
coordinate: { type: [Number], required: false },
|
||||
velocity: { type: Number, required: false },
|
||||
battery: { type: Number, required: false },
|
||||
magneticField: { type: Number, required: false },
|
||||
phone: { type: Phone, required: true, default: 'user' }
|
||||
}, {
|
||||
timestamps: {
|
||||
createdAt: true
|
||||
}
|
||||
});
|
||||
|
||||
export { schemaTrack };
|
||||
18
backend/package-lock.json
generated
18
backend/package-lock.json
generated
@@ -96,6 +96,15 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/cors": {
|
||||
"version": "2.8.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.8.tgz",
|
||||
"integrity": "sha512-fO3gf3DxU2Trcbr75O7obVndW/X5k8rJNZkLXlQWStTHhP71PkRqjwPIEI0yMnJdg9R9OasjU+Bsr+Hr1xy/0w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"@types/dotenv": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-8.2.0.tgz",
|
||||
@@ -868,6 +877,15 @@
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"cors": {
|
||||
"version": "2.8.5",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||
"requires": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
}
|
||||
},
|
||||
"crypto-random-string": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"argon2": "^0.27.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"chalk": "^4.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"figlet": "^1.5.0",
|
||||
@@ -43,6 +44,7 @@
|
||||
"concurrently": "^5.3.0",
|
||||
"nodemon": "^2.0.5",
|
||||
"@types/jsonwebtoken": "8.5.0",
|
||||
"@types/amqplib": "0.5.14"
|
||||
"@types/amqplib": "0.5.14",
|
||||
"@types/cors": "2.8.8"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user