Switch from RabbitMQ to server-sent events
(not fully working yet)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -107,3 +107,4 @@ dist
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
android_new
|
||||
@@ -44,7 +44,8 @@ dependencies {
|
||||
implementation 'com.squareup.moshi:moshi:1.11.0'
|
||||
implementation 'com.squareup.moshi:moshi-kotlin:1.11.0'
|
||||
implementation 'com.rabbitmq:amqp-client:5.9.0'
|
||||
implementation "com.squareup.okhttp3:okhttp:4.9.0"
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp-sse:4.9.1'
|
||||
testImplementation 'junit:junit:4.+'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package de.nicolasklier.livebeat
|
||||
|
||||
import android.util.Log
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.platform.Platform
|
||||
import okhttp3.internal.platform.Platform.Companion.INFO
|
||||
import okhttp3.sse.EventSource
|
||||
import okhttp3.sse.EventSourceListener
|
||||
import okio.IOException
|
||||
import org.jetbrains.annotations.Nullable
|
||||
import java.util.concurrent.BlockingQueue
|
||||
import java.util.concurrent.LinkedBlockingDeque
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class EventSourceRecorder : EventSourceListener() {
|
||||
private val events: BlockingQueue<Any> = LinkedBlockingDeque()
|
||||
|
||||
override fun onOpen(eventSource: EventSource, response: Response) {
|
||||
Log.i("SSE", "Connection opened!")
|
||||
}
|
||||
|
||||
override fun onEvent(
|
||||
eventSource: EventSource, @Nullable id: String?, @Nullable type: String?,
|
||||
data: String
|
||||
) {
|
||||
Log.i("SSE", "onEvent: " + data)
|
||||
}
|
||||
|
||||
override fun onClosed(eventSource: EventSource) {
|
||||
Log.i("SSE", "Connection to SSE got closed!")
|
||||
events.add(Closed())
|
||||
}
|
||||
|
||||
fun onFailure(
|
||||
eventSource: EventSource?,
|
||||
@Nullable t: Throwable
|
||||
) {
|
||||
Platform.get().log("[ES] onFailure", INFO, t)
|
||||
}
|
||||
|
||||
private fun nextEvent(): Any {
|
||||
return try {
|
||||
val event: Any = events.poll(10, TimeUnit.SECONDS)
|
||||
?: throw AssertionError("Timed out waiting for event.")
|
||||
event
|
||||
} catch (e: InterruptedException) {
|
||||
throw AssertionError(e)
|
||||
}
|
||||
}
|
||||
|
||||
internal class Open(val eventSource: EventSource?, response: Response) {
|
||||
val response: Response
|
||||
override fun toString(): String {
|
||||
return "Open[$response]"
|
||||
}
|
||||
|
||||
init {
|
||||
this.response = response
|
||||
}
|
||||
}
|
||||
|
||||
internal class Failure(val t: Throwable, response: Response?) {
|
||||
val response: Response?
|
||||
val responseBody: String?
|
||||
override fun toString(): String {
|
||||
return if (response == null) {
|
||||
"Failure[$t]"
|
||||
} else "Failure[$response]"
|
||||
}
|
||||
|
||||
init {
|
||||
this.response = response
|
||||
var responseBody: String? = null
|
||||
if (response != null) {
|
||||
try {
|
||||
responseBody = response.body.toString()
|
||||
} catch (ignored: IOException) {
|
||||
}
|
||||
}
|
||||
this.responseBody = responseBody
|
||||
}
|
||||
}
|
||||
|
||||
internal class Closed {
|
||||
override fun toString(): String {
|
||||
return "Closed[]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package de.nicolasklier.livebeat
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import okhttp3.sse.EventSource
|
||||
import okhttp3.sse.EventSources
|
||||
import java.lang.Error
|
||||
import java.net.ConnectException
|
||||
|
||||
class HttpRequests {
|
||||
companion object {
|
||||
fun get(url: String, sendToken: Boolean = true): Response {
|
||||
val client = OkHttpClient()
|
||||
var req = Request.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
if (sendToken) {
|
||||
req = req.newBuilder().addHeader("token", MainActivity.TOKEN).build();
|
||||
}
|
||||
return client.newCall(req).execute();
|
||||
}
|
||||
|
||||
fun post(url: String, body: String, sendToken: Boolean = true): Response {
|
||||
try {
|
||||
val client = OkHttpClient()
|
||||
var req = Request.Builder()
|
||||
.url(url)
|
||||
.post(
|
||||
(body).toRequestBody()
|
||||
)
|
||||
.header("Content-Type", "application/json")
|
||||
.build()
|
||||
|
||||
if (sendToken) {
|
||||
req = req.newBuilder().addHeader("token", MainActivity.TOKEN).build()
|
||||
}
|
||||
return client.newCall(req).execute()
|
||||
} catch (e: ConnectException) {
|
||||
throw Error("Connection to $url couldn't be made: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun sse(url: String) {
|
||||
val client = OkHttpClient()
|
||||
val req = Request.Builder().url(url).build();
|
||||
|
||||
val handler = EventSourceRecorder()
|
||||
val factory = EventSources.createFactory(client)
|
||||
val sse = factory.newEventSource(req, handler)
|
||||
|
||||
sse.request()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -31,6 +32,7 @@ import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.net.ConnectException
|
||||
import java.util.logging.Logger
|
||||
|
||||
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
|
||||
@@ -49,13 +51,13 @@ class MainActivity : AppCompatActivity() {
|
||||
if (TOKEN == "") return;
|
||||
Thread(Runnable {
|
||||
val androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
|
||||
val client = OkHttpClient()
|
||||
/*val client = OkHttpClient()
|
||||
val req = Request.Builder()
|
||||
.url("$API_URL/phone/$androidId")
|
||||
.header("token", TOKEN)
|
||||
.get()
|
||||
.build()
|
||||
val response = client.newCall(req).execute()
|
||||
.build()*/
|
||||
val response = HttpRequests.get("$API_URL/phone/$androidId")
|
||||
|
||||
if (response.code != 200) {
|
||||
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Device isn't registered yet. Registering ...", Snackbar.LENGTH_SHORT)
|
||||
@@ -75,7 +77,7 @@ class MainActivity : AppCompatActivity() {
|
||||
val phoneToJson = moshi.adapter(Phone::class.java)
|
||||
val json = phoneToJson.toJson(phone)
|
||||
|
||||
val createPhone = Request.Builder()
|
||||
/*val createPhone = Request.Builder()
|
||||
.url("$API_URL/phone")
|
||||
.post(
|
||||
(json).toRequestBody()
|
||||
@@ -83,7 +85,8 @@ class MainActivity : AppCompatActivity() {
|
||||
.header("Content-Type", "application/json")
|
||||
.header("token", TOKEN)
|
||||
.build()
|
||||
client.newCall(createPhone).execute()
|
||||
client.newCall(createPhone).execute()*/
|
||||
HttpRequests.post("$API_URL/phone", json);
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
@@ -95,7 +98,7 @@ class MainActivity : AppCompatActivity() {
|
||||
setContentView(R.layout.activity_main)
|
||||
setSupportActionBar(findViewById(R.id.toolbar))
|
||||
|
||||
val process = Runtime.getRuntime().exec("su")
|
||||
//val process = Runtime.getRuntime().exec("su")
|
||||
|
||||
// Check authorization
|
||||
val backendChecks = Thread(Runnable {
|
||||
@@ -104,15 +107,40 @@ class MainActivity : AppCompatActivity() {
|
||||
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
|
||||
val jsonToLogin = moshi.adapter(Login::class.java)
|
||||
|
||||
val token = "{ \"username\": \"" + username + "\"," +
|
||||
"\"password\": \"" + password + "\" }"
|
||||
try {
|
||||
HttpRequests.post("$API_URL/user/login", token)
|
||||
} catch (e: Error) {
|
||||
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Backend server is not available", Snackbar.LENGTH_SHORT)
|
||||
.setBackgroundTint(Color.RED)
|
||||
.setActionTextColor(Color.WHITE)
|
||||
.show();
|
||||
return@Runnable
|
||||
}
|
||||
val client = OkHttpClient()
|
||||
val req = Request.Builder()
|
||||
.url("$API_URL/user/login")
|
||||
.post(
|
||||
("{ \"username\": \"" + username + "\"," +
|
||||
"\"password\": \"" + password + "\" }").toRequestBody()
|
||||
(token).toRequestBody()
|
||||
)
|
||||
.header("Content-Type", "application/json")
|
||||
.build()
|
||||
|
||||
// Check if server is available.
|
||||
try {
|
||||
val testReq = Request.Builder()
|
||||
.url("$API_URL/user/login")
|
||||
.post(
|
||||
("{ \"username\": \"" + username + "\"," +
|
||||
"\"password\": \"" + password + "\" }").toRequestBody()
|
||||
)
|
||||
.header("Content-Type", "application/json")
|
||||
.build()
|
||||
client.newCall(testReq).execute();
|
||||
} catch (e: ConnectException) {
|
||||
}
|
||||
|
||||
val loginResponse = client.newCall(req).execute()
|
||||
val responseBody = loginResponse.body!!.string()
|
||||
|
||||
@@ -141,8 +169,10 @@ class MainActivity : AppCompatActivity() {
|
||||
val userInfoResponseBody = userinfoResponse.body!!.string()
|
||||
USER = jsonToUser.fromJson(userInfoResponseBody)
|
||||
|
||||
val intent = Intent(this, TrackerService::class.java)
|
||||
|
||||
// Only start service if authentication went good.
|
||||
startService(Intent(this, TrackerService::class.java))
|
||||
startService(intent)
|
||||
|
||||
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Login succeeded", Snackbar.LENGTH_SHORT)
|
||||
.setBackgroundTint(Color.GREEN)
|
||||
@@ -171,17 +201,8 @@ class MainActivity : AppCompatActivity() {
|
||||
this.broadcastReceiver = object : BroadcastReceiver() {
|
||||
@SuppressLint("CutPasteId")
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val statusRabbit = intent.getBooleanExtra("statusRabbit", false)
|
||||
val statusHttp = intent.getIntExtra("statusHttp", 404)
|
||||
|
||||
if (statusRabbit) {
|
||||
findViewById<TextView>(R.id.rabbitStatus).text = "connected"
|
||||
findViewById<TextView>(R.id.rabbitStatus).setTextColor(Color.GREEN)
|
||||
} else {
|
||||
findViewById<TextView>(R.id.rabbitStatus).text = "disconnected"
|
||||
findViewById<TextView>(R.id.rabbitStatus).setTextColor(Color.RED)
|
||||
}
|
||||
|
||||
/*if (statusHttp == 200) {
|
||||
findViewById<TextView>(R.id.httpStatus).text = "ONLINE (no login)"
|
||||
findViewById<TextView>(R.id.httpStatus).setTextColor(Color.CYAN)
|
||||
|
||||
@@ -27,7 +27,7 @@ class User(
|
||||
val type: String,
|
||||
val lastLogin: String,
|
||||
val twoFASecret: String?,
|
||||
val brokerToken: String,
|
||||
val eventToken: String,
|
||||
val createdAt: String
|
||||
)
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import android.content.pm.PackageManager
|
||||
import android.graphics.Color
|
||||
import android.location.LocationManager
|
||||
import android.os.BatteryManager
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
@@ -18,14 +17,9 @@ import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.rabbitmq.client.Channel
|
||||
import com.rabbitmq.client.Connection
|
||||
import com.rabbitmq.client.ConnectionFactory
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeoutException
|
||||
|
||||
class TrackerService : Service() {
|
||||
|
||||
@@ -36,38 +30,9 @@ class TrackerService : Service() {
|
||||
return null
|
||||
}
|
||||
|
||||
private fun connectWithBroker() {
|
||||
// This thread only connects to RabbitMQ
|
||||
val connectionThread = Thread(Runnable {
|
||||
val client = OkHttpClient()
|
||||
|
||||
val factory = ConnectionFactory()
|
||||
factory.username = MainActivity.USER!!.name
|
||||
factory.password = MainActivity.USER!!.brokerToken
|
||||
factory.virtualHost = "/"
|
||||
factory.host = "192.168.178.26"
|
||||
factory.port = 5672
|
||||
factory.isAutomaticRecoveryEnabled = true
|
||||
try {
|
||||
conn[0] = factory.newConnection()
|
||||
channel[0] = conn[0]?.createChannel()
|
||||
|
||||
val intent = Intent("de.nicolasklier.livebeat")
|
||||
val bundle = Bundle()
|
||||
bundle.putBoolean("statusRabbit", true)
|
||||
intent.putExtras(bundle)
|
||||
this.sendBroadcast(intent)
|
||||
|
||||
channel[0]?.queueDeclare("tracker-" + factory.username, true, false, false, null)
|
||||
//channel[0]?.basicPublish("", "Tracker", null, "Test message".toByteArray())
|
||||
Log.i("RabbitMQ", "run: Published test message")
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: TimeoutException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
})
|
||||
connectionThread.start()
|
||||
@SuppressLint("CheckResult")
|
||||
private fun subscribeToEvents() {
|
||||
HttpRequests.sse("http://192.168.178.26/user/events?token=${MainActivity.USER?.eventToken}")
|
||||
}
|
||||
|
||||
@SuppressLint("HardwareIds")
|
||||
@@ -105,7 +70,7 @@ class TrackerService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
connectWithBroker()
|
||||
subscribeToEvents()
|
||||
startForeground()
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
@@ -68,31 +68,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/rabbitText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal|center_vertical"
|
||||
android:layout_weight="0"
|
||||
android:text="RabbitMQ"
|
||||
android:textColor="@color/white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/rabbitStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="20dp"
|
||||
android:layout_weight="0"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:text="disconnected"
|
||||
android:textAlignment="center"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/orange"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
android:orientation="horizontal"/>
|
||||
|
||||
<Space
|
||||
android:layout_width="match_parent"
|
||||
|
||||
8
backend/.idea/.gitignore
generated
vendored
Normal file
8
backend/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
8
backend/.idea/backend.iml
generated
Normal file
8
backend/.idea/backend.iml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
backend/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
backend/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
4
backend/.idea/misc.xml
generated
Normal file
4
backend/.idea/misc.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
backend/.idea/modules.xml
generated
Normal file
8
backend/.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/backend.iml" filepath="$PROJECT_DIR$/.idea/backend.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
backend/.idea/vcs.xml
generated
Normal file
6
backend/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -7,13 +7,13 @@ import * as figlet from 'figlet';
|
||||
import * as mongoose from 'mongoose';
|
||||
import { exit } from 'process';
|
||||
import * as winston from 'winston';
|
||||
|
||||
import { config } from './config';
|
||||
import { GetBeat, GetBeatStats } from './endpoints/beat';
|
||||
import { getNotification } from './endpoints/notification';
|
||||
import { GetPhone, PostPhone } from './endpoints/phone';
|
||||
import { DeleteUser, GetUser, LoginRabbitUser, LoginUser, MW_User, PatchUser, PostUser, Resource, Topic, VHost } from './endpoints/user';
|
||||
import { DeleteUser, GetUser, LoginUser, MW_User, PatchUser, PostUser, UserEvents } from './endpoints/user';
|
||||
import { hashPassword, randomPepper, randomString } from './lib/crypto';
|
||||
import { EventManager } from './lib/eventManager';
|
||||
import { RabbitMQ } from './lib/rabbit';
|
||||
import { UserType } from './models/user/user.interface';
|
||||
import { User } from './models/user/user.model';
|
||||
@@ -28,6 +28,7 @@ export const IS_DEBUG = process.env.DEBUG == 'true';
|
||||
|
||||
export let logger: winston.Logger;
|
||||
export let rabbitmq: RabbitMQ;
|
||||
export let eventManager: EventManager = new EventManager();
|
||||
|
||||
async function run() {
|
||||
const { combine, timestamp, label, printf, prettyPrint } = winston.format;
|
||||
@@ -108,7 +109,7 @@ async function run() {
|
||||
await User.create({
|
||||
name: 'admin',
|
||||
password: await hashPassword(randomPassword + salt + randomPepper()),
|
||||
brokerToken: randomString(16),
|
||||
eventToken: randomString(16),
|
||||
salt,
|
||||
createdAt: Date.now(),
|
||||
lastLogin: 0,
|
||||
@@ -139,15 +140,12 @@ async function run() {
|
||||
|
||||
app.get('/', (req, res) => res.status(200).send('OK'));
|
||||
|
||||
// User authentication
|
||||
// User authentication & actions
|
||||
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));
|
||||
|
||||
// CRUD user
|
||||
app.get('/user/notification', MW_User, (req, res) => getNotification(req, res)); // Notifications
|
||||
app.get('/user/events', (req, res) => UserEvents(req, res));
|
||||
app.get('/user/', MW_User, (req, res) => GetUser(req, res));
|
||||
app.post('/user/', MW_User, (req, res) => PostUser(req, res));
|
||||
app.get('/user/:id', MW_User, (req, res) => GetUser(req, res));
|
||||
@@ -171,8 +169,8 @@ async function run() {
|
||||
* Message broker
|
||||
*/
|
||||
rabbitmq = new RabbitMQ();
|
||||
await rabbitmq.init();
|
||||
logger.info("Connected with message broker.");
|
||||
//await rabbitmq.init();
|
||||
//logger.info("Connected with message broker.");
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -16,6 +16,10 @@ export const config: IConfig = {
|
||||
host: "0.0.0.0"
|
||||
}
|
||||
}
|
||||
/**
|
||||
* END OF CONFIG
|
||||
* ====================
|
||||
*/
|
||||
|
||||
export interface IConfig {
|
||||
authentification: {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Response } from "express";
|
||||
import { eventManager, logger } from "../app";
|
||||
import { LivebeatRequest } from "../lib/request";
|
||||
import { IBeat } from "../models/beat/beat.interface";
|
||||
import { Beat } from "../models/beat/beat.model.";
|
||||
import { ISeverity } from "../models/notifications/notification.interface";
|
||||
import { Phone } from "../models/phone/phone.model";
|
||||
|
||||
const timeouts: Map<string, NodeJS.Timeout> = new Map<string, NodeJS.Timeout>();
|
||||
|
||||
export async function GetBeatStats(req: LivebeatRequest, res: Response) {
|
||||
const phones = await Phone.find({ user: req.user?._id });
|
||||
const perPhone: any = {};
|
||||
@@ -20,23 +24,26 @@ export async function GetBeatStats(req: LivebeatRequest, res: Response) {
|
||||
}
|
||||
|
||||
export async function GetBeat(req: LivebeatRequest, res: Response) {
|
||||
const from: number = Number(req.query.from);
|
||||
const to: number = Number(req.query.to);
|
||||
const from: number = Number(req.query.from || 0);
|
||||
const to: number = Number(req.query.to || Date.now() / 1000);
|
||||
const limit: number = Number(req.query.limit || 10000);
|
||||
const sort: number = Number(req.query.sort || 1); // Either -1 or 1
|
||||
const phoneId = req.query.phoneId;
|
||||
|
||||
// Grab default phone if non was provided.
|
||||
const phone = req.query.phone === undefined ? await Phone.findOne({ user: req.user?._id }) : await Phone.findOne({ _id: phoneId, user: req.user?._id });
|
||||
let beats: IBeat[] = []
|
||||
let beats: IBeat[] = [];
|
||||
|
||||
//console.log(from, to);
|
||||
//console.log(`Search from ${new Date(from).toString()} to ${new Date(to * 1000).toString()}`);
|
||||
|
||||
if (phone !== null) {
|
||||
beats = await Beat.find(
|
||||
{
|
||||
phone: phone._id,
|
||||
createdAt: {
|
||||
$gte: new Date((from | 0) * 1000),
|
||||
$lte: new Date((to | Date.now() /1000) * 1000)
|
||||
$gte: new Date((from)),
|
||||
$lte: new Date(to * 1000)
|
||||
}
|
||||
}).sort({ _id: sort }).limit(limit);
|
||||
res.status(200).send(beats);
|
||||
@@ -44,3 +51,61 @@ export async function GetBeat(req: LivebeatRequest, res: Response) {
|
||||
res.status(404).send({ message: 'Phone not found' });
|
||||
}
|
||||
}
|
||||
|
||||
export async function AddBeat(req: LivebeatRequest, res: Response) {
|
||||
const beat = req.body as IBeat;
|
||||
const androidId = req.headers.deviceId as string;
|
||||
|
||||
if (androidId === undefined) {
|
||||
res.status(401).send({ message: 'Device id is missing' });
|
||||
}
|
||||
|
||||
// Get phone
|
||||
const phone = await Phone.findOne({ androidId });
|
||||
if (phone == undefined) {
|
||||
logger.warning(`Received beat from unknown device with id ${androidId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let newBeat;
|
||||
if (beat.coordinate !== undefined && beat.accuracy !== undefined) {
|
||||
logger.info(`New beat from ${phone.displayName} => ${beat.coordinate[0]}, ${beat.coordinate[1]} | Height: ${beat.coordinate[3]}m | Speed: ${beat.coordinate[4]} | Accuracy: ${beat.accuracy}% | Battery: ${beat.battery}%`);
|
||||
|
||||
newBeat = await Beat.create({
|
||||
phone: phone._id,
|
||||
// [latitude, longitude, altitude]
|
||||
coordinate: [beat.coordinate[0], beat.coordinate[1], beat.coordinate[2]],
|
||||
accuracy: beat.coordinate[3],
|
||||
speed: beat.coordinate[4],
|
||||
battery: beat.battery,
|
||||
createdAt: Date.now()
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
newBeat = await Beat.create({
|
||||
phone: phone._id,
|
||||
battery: beat.battery,
|
||||
createdAt: Date.now()
|
||||
});
|
||||
|
||||
// Broadcast if device became active
|
||||
if (timeouts.has(phone.id)) {
|
||||
clearTimeout(timeouts.get(phone.id)!!);
|
||||
} else {
|
||||
phone.active = true;
|
||||
await phone.save();
|
||||
|
||||
eventManager.push('phone_alive', phone.toJSON(), phone.user);
|
||||
}
|
||||
|
||||
const timeoutTimer = setTimeout(async () => {
|
||||
eventManager.push('phone_dead', phone.toJSON(), phone.user, ISeverity.WARN);
|
||||
timeouts.delete(phone.id);
|
||||
phone.active = false;
|
||||
await phone.save();
|
||||
}, 60_000);
|
||||
timeouts.set(phone.id, timeoutTimer);
|
||||
|
||||
eventManager.push('beat', newBeat.toJSON(), phone.user);
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { Beat } from "../models/beat/beat.model.";
|
||||
import { Phone } from "../models/phone/phone.model";
|
||||
|
||||
|
||||
|
||||
export async function GetPhone(req: LivebeatRequest, res: Response) {
|
||||
const phoneId: String = req.params['id'];
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { decode, sign, verify } from 'jsonwebtoken';
|
||||
|
||||
import { JWT_SECRET, logger, RABBITMQ_URI } from '../app';
|
||||
import { eventManager, JWT_SECRET, logger, RABBITMQ_URI } from '../app';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import { config } from '../config';
|
||||
import { hashPassword, randomPepper, randomString, verifyPassword } from '../lib/crypto';
|
||||
@@ -51,7 +51,7 @@ export async function PostUser(req: LivebeatRequest, res: Response) {
|
||||
}
|
||||
|
||||
const salt = randomString(config.authentification.salt_length);
|
||||
const brokerToken = randomString(16);
|
||||
const eventToken = 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;
|
||||
@@ -61,7 +61,7 @@ export async function PostUser(req: LivebeatRequest, res: Response) {
|
||||
name,
|
||||
password: hashedPassword,
|
||||
salt,
|
||||
brokerToken,
|
||||
eventToken,
|
||||
type,
|
||||
lastLogin: new Date(0)
|
||||
});
|
||||
@@ -72,6 +72,27 @@ export async function PostUser(req: LivebeatRequest, res: Response) {
|
||||
res.status(200).send({ setupToken });
|
||||
}
|
||||
|
||||
export async function UserEvents(req: LivebeatRequest, res: Response) {
|
||||
if (req.query.token === undefined) {
|
||||
res.status(401).send({ message: 'You need to define your event token.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const eventToken = req.query.token as string;
|
||||
const user = await User.findOne({ eventToken });
|
||||
|
||||
if (user === null) {
|
||||
res.status(401).send({ message: 'This event token is not valid.' });
|
||||
return;
|
||||
}
|
||||
|
||||
eventManager.join(user.id, res);
|
||||
}
|
||||
|
||||
export async function UserSubscribeEvent(req: Request, res: Response) {
|
||||
|
||||
}
|
||||
|
||||
export async function DeleteUser(req: Request, res: Response) {
|
||||
|
||||
}
|
||||
@@ -120,159 +141,6 @@ 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;
|
||||
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);
|
||||
|
||||
const username = req.query.username;
|
||||
const routingKey = req.query.routing_key;
|
||||
|
||||
if (routingKey === undefined || username === undefined) {
|
||||
res.send('deny');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's us
|
||||
if (username.toString() == 'backend') {
|
||||
res.status(200).send('allow');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user exists
|
||||
const user = await User.findOne({ name: username.toString() });
|
||||
if (user === null) {
|
||||
res.send('deny');
|
||||
return;
|
||||
}
|
||||
|
||||
if (routingKey !== user.id) {
|
||||
res.send('deny');
|
||||
return;
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
170
backend/lib/eventManager.ts
Normal file
170
backend/lib/eventManager.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { Response } from "express";
|
||||
import { logger } from "../app";
|
||||
import { ISeverity, NotificationType, PublicNotificationType } from "../models/notifications/notification.interface";
|
||||
import { addNotification } from "../models/notifications/notification.model";
|
||||
import { IPhone } from "../models/phone/phone.interface";
|
||||
import { IUser } from "../models/user/user.interface";
|
||||
import { User } from "../models/user/user.model";
|
||||
import { randomString } from "./crypto";
|
||||
|
||||
/**
|
||||
* This class stores one specific client.
|
||||
*/
|
||||
export class Client {
|
||||
id: string;
|
||||
userId: string;
|
||||
stream: Response;
|
||||
|
||||
constructor(stream: Response, userId: string) {
|
||||
this.id = randomString(16);
|
||||
this.userId = userId;
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
send(type: NotificationType, data: any) {
|
||||
this.stream.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`);
|
||||
}
|
||||
|
||||
async getUser() {
|
||||
return await User.findById(this.userId);
|
||||
}
|
||||
}
|
||||
|
||||
export class Clients {
|
||||
private clients: Client[];
|
||||
|
||||
constructor(clients: Client[]) {
|
||||
this.clients = clients;
|
||||
}
|
||||
|
||||
getClientsByUser(userId: string) {
|
||||
const userClients = [];
|
||||
for (let i = 0; i < this.clients.length; i++) {
|
||||
if (this.clients[i].userId === userId) {
|
||||
userClients.push(this.clients[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return userClients;
|
||||
}
|
||||
|
||||
closeAllClientsByUser(userId: string) {
|
||||
this.getClientsByUser(userId).forEach(client => {
|
||||
client.stream.end();
|
||||
});
|
||||
}
|
||||
|
||||
addClient(client: Client) {
|
||||
this.clients.push(client);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
getClients() {
|
||||
return this.clients;
|
||||
}
|
||||
}
|
||||
|
||||
export class EventManager {
|
||||
constructor() {
|
||||
setInterval(() => {
|
||||
this.broadcast('info', { message: "Test" });
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// This map stores a open data stream and it's associated room.
|
||||
private clients: Clients = new Clients([]);
|
||||
|
||||
private addClient(stream: Response, userId: string) {
|
||||
this.clients.addClient(new Client(stream, userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a client to a specific room
|
||||
* @param room Used as an id for the specific room
|
||||
* @param stream A open connection to the user
|
||||
*/
|
||||
async join(userId: string, stream: Response) {
|
||||
if (stream.req == undefined) {
|
||||
stream.send(500);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check user
|
||||
const user = await User.findById(userId);
|
||||
if (user === null) {
|
||||
stream.send(401);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure to keep the connection open
|
||||
stream.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive'
|
||||
});
|
||||
|
||||
this.addClient(stream, userId);
|
||||
logger.debug(`Client ${stream.req.hostname} of user ${user.name} joined.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a new event into a specific room
|
||||
* @param event Type of the event
|
||||
* @param data Content of the event
|
||||
* @param selector Room to push in. If empty then it will be a public broadcast to anyone.
|
||||
*/
|
||||
push(type: NotificationType, data: any, user: IUser, severity = ISeverity.INFO) {
|
||||
let clients = this.clients.getClientsByUser(user.id);
|
||||
if (clients === undefined) return;
|
||||
|
||||
/* Manage notifications */
|
||||
if (type != 'beat' && user !== undefined) {
|
||||
if (type == 'phone_alive' || type == 'phone_dead') {
|
||||
addNotification(type, severity, ((data as IPhone)._id), user);
|
||||
}
|
||||
}
|
||||
|
||||
data = { type, severity, ...data };
|
||||
|
||||
clients.forEach((client) => {
|
||||
client.stream.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`);
|
||||
});
|
||||
|
||||
if (user === undefined) {
|
||||
logger.debug(`Broadcasted event ${type} to all users (${clients.length} clients affected)`);
|
||||
} else {
|
||||
logger.debug(`Broadcasted event ${type} to user ${user.id} (${clients.length} clients affected)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Very much like push() but it will send this message to **every connected client!**
|
||||
*/
|
||||
broadcast(type: PublicNotificationType, data: any) {
|
||||
this.clients.getClients().forEach(async client => {
|
||||
console.log(`Send ${JSON.stringify(data)} (of type ${type}) to a client of user ${(await client.getUser())?.name}`);
|
||||
client.stream.write(`event: message\ndata: ${JSON.stringify(data)}\n\n`);
|
||||
});
|
||||
|
||||
logger.debug(`Broadcasted event ${type} to all users (${this.clients.getClients().length} clients affected)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* End the communication with a specific client.
|
||||
*/
|
||||
end(stream: Response, userId: string) {
|
||||
stream.end();
|
||||
|
||||
logger.debug(`End connection with ${stream.req?.hostname} (user: ${userId})`);
|
||||
}
|
||||
|
||||
static buildEventTypeName(type: EventType, user: IUser) {
|
||||
return `${type}-${user}`;
|
||||
}
|
||||
}
|
||||
|
||||
export type EventType =
|
||||
| 'tracker' // Receive just the gps location of a specific user.
|
||||
| 'user' // Receive user updates.
|
||||
| 'all'; // Receive all above events.
|
||||
@@ -63,7 +63,7 @@ export class RabbitMQ {
|
||||
this.timeouts.delete(phone.id);
|
||||
phone.active = false;
|
||||
await phone.save();
|
||||
}, 30_000);
|
||||
}, 60_000);
|
||||
this.timeouts.set(phone.id, timeoutTimer);
|
||||
|
||||
this.publish(phone.user.toString(), newBeat.toJSON(), 'beat');
|
||||
|
||||
@@ -4,8 +4,8 @@ import { IPhone } from '../phone/phone.interface';
|
||||
export interface IBeat extends Document {
|
||||
// [latitude, longitude, altitude, accuracy, speed]
|
||||
coordinate?: number[],
|
||||
accuracy: number,
|
||||
speed: number,
|
||||
accuracy?: number,
|
||||
speed?: number,
|
||||
battery?: number,
|
||||
phone: IPhone,
|
||||
createdAt?: Date
|
||||
|
||||
@@ -8,7 +8,8 @@ export enum ISeverity {
|
||||
ERROR = 3
|
||||
}
|
||||
|
||||
export type NotificationType = 'beat' | 'phone_alive' | 'phone_dead' | 'phone_register' | 'panic';
|
||||
export type NotificationType = 'beat' | 'phone_alive' | 'phone_dead' | 'phone_register' | 'panic' | 'test';
|
||||
export type PublicNotificationType = 'shutdown' | 'restart' | 'warning' | 'error' | 'info';
|
||||
|
||||
export interface INotification extends Document {
|
||||
type: NotificationType;
|
||||
|
||||
@@ -13,6 +13,6 @@ export interface IUser extends Document {
|
||||
type: UserType,
|
||||
lastLogin: Date,
|
||||
twoFASecret?: string,
|
||||
brokerToken: string,
|
||||
eventToken: string,
|
||||
createdAt?: Date
|
||||
}
|
||||
@@ -6,7 +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 },
|
||||
eventToken: { type: String, required: true },
|
||||
lastLogin: { type: Date, required: true, default: Date.now },
|
||||
}, {
|
||||
timestamps: {
|
||||
|
||||
3774
backend/package-lock.json
generated
3774
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@
|
||||
"@types/node": "^14.14.9",
|
||||
"amqplib": "^0.6.0",
|
||||
"argon2": "^0.27.0",
|
||||
"bi-directional-map": "^1.0.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"chalk": "^4.1.0",
|
||||
"cors": "^2.8.5",
|
||||
@@ -34,20 +35,20 @@
|
||||
"winston": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/amqplib": "0.5.14",
|
||||
"@types/argon2": "0.15.0",
|
||||
"@types/body-parser": "1.19.0",
|
||||
"@types/chalk": "2.2.0",
|
||||
"@types/cors": "2.8.8",
|
||||
"@types/dotenv": "8.2.0",
|
||||
"@types/express": "4.17.8",
|
||||
"@types/figlet": "1.2.0",
|
||||
"@types/jsonwebtoken": "8.5.0",
|
||||
"@types/moment": "2.13.0",
|
||||
"@types/mongoose": "5.7.36",
|
||||
"@types/typescript": "2.0.0",
|
||||
"@types/winston": "2.4.4",
|
||||
"concurrently": "^5.3.0",
|
||||
"nodemon": "^2.0.5",
|
||||
"@types/jsonwebtoken": "8.5.0",
|
||||
"@types/amqplib": "0.5.14",
|
||||
"@types/cors": "2.8.8",
|
||||
"@types/moment": "2.13.0"
|
||||
"nodemon": "^2.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
17692
frontend/package-lock.json
generated
17692
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -36,7 +36,6 @@
|
||||
"moment": "^2.29.1",
|
||||
"ng2-charts": "^2.4.2",
|
||||
"ngx-mapbox-gl": "^4.8.1",
|
||||
"ngx-mqtt": "^7.0.14",
|
||||
"rxjs": "~6.6.0",
|
||||
"tslib": "^2.0.0",
|
||||
"zone.js": "~0.10.2"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AfterContentInit, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { resolve } from 'dns';
|
||||
import { APIService, UserType } from '../api.service';
|
||||
import { AlertService } from '../_alert/alert.service';
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MqttService } from 'ngx-mqtt';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import * as moment from 'moment';
|
||||
import { error } from 'protractor';
|
||||
import { AlertService } from './_alert/alert.service';
|
||||
import { AlertService, AlertType } from './_alert/alert.service';
|
||||
|
||||
/*
|
||||
* ==========================
|
||||
* DEFINITION OF TYPE
|
||||
*/
|
||||
export interface ILogin {
|
||||
@@ -44,7 +43,7 @@ export enum UserType {
|
||||
export interface IUser {
|
||||
_id: string;
|
||||
name: string;
|
||||
brokerToken: string;
|
||||
eventToken: string;
|
||||
type: UserType;
|
||||
lastLogin: Date;
|
||||
twoFASecret?: string;
|
||||
@@ -84,8 +83,13 @@ export interface INotification extends Document {
|
||||
user: IUser;
|
||||
}
|
||||
|
||||
interface CustomEvent extends Event {
|
||||
data: any;
|
||||
}
|
||||
|
||||
/*
|
||||
* END OF THE DEFINITION OF TYPE
|
||||
* END OF THE TYPE DEFINITION
|
||||
* ==========================
|
||||
*/
|
||||
|
||||
@Injectable({
|
||||
@@ -94,9 +98,9 @@ export interface INotification extends Document {
|
||||
export class APIService {
|
||||
|
||||
private token: string;
|
||||
private events: EventSource | undefined;
|
||||
|
||||
username: string;
|
||||
rabbitmq: any;
|
||||
|
||||
// Passthough data (not useful for api but a way for components to share data)
|
||||
showFilter = true;
|
||||
@@ -112,7 +116,7 @@ export class APIService {
|
||||
user: IUser = {
|
||||
_id: '',
|
||||
name: '',
|
||||
brokerToken: '',
|
||||
eventToken: '',
|
||||
lastLogin: new Date(2020, 3, 1),
|
||||
type: UserType.GUEST,
|
||||
createdAt: new Date(),
|
||||
@@ -128,42 +132,66 @@ export class APIService {
|
||||
|
||||
API_ENDPOINT = 'http://192.168.178.26:8040';
|
||||
|
||||
constructor(private httpClient: HttpClient, private mqtt: MqttService, private alert: AlertService) { }
|
||||
constructor(private httpClient: HttpClient, private alert: AlertService) { }
|
||||
|
||||
private mqttInit(): void {
|
||||
// Connect with RabbitMQ after we received our user information
|
||||
this.mqtt.connect({
|
||||
hostname: '192.168.178.26',
|
||||
port: 15675,
|
||||
protocol: 'ws',
|
||||
path: '/ws',
|
||||
username: this.user.name,
|
||||
password: this.user.brokerToken
|
||||
});
|
||||
/**
|
||||
* This functions opens a new http connection that stays open, forever, to receive events from the backend.
|
||||
*/
|
||||
async subscribeToEvents() {
|
||||
let shownError = false;
|
||||
|
||||
this.mqtt.observe(this.user._id).subscribe(async message => {
|
||||
if (message !== undefined || message !== null) {
|
||||
const obj = JSON.parse(message.payload.toString());
|
||||
console.log('Received message:', obj);
|
||||
// If there is already a event, close it.
|
||||
if (this.events !== undefined) {
|
||||
this.events.close();
|
||||
}
|
||||
|
||||
if (obj.type === 'beat') {
|
||||
this.events = new EventSource(`${this.API_ENDPOINT}/user/events?token=${this.user.eventToken}`);
|
||||
|
||||
this.events.onopen = event => {
|
||||
console.info('Connection to event stream is open. Awaiting incoming events ...');
|
||||
shownError = false;
|
||||
};
|
||||
|
||||
this.events.onerror = error => {
|
||||
if (shownError) return;
|
||||
console.error('Connection to event stream has failed! Error: ' + error);
|
||||
this.alert.error('Could not subscribe to events', 'Events');
|
||||
|
||||
shownError = true;
|
||||
}
|
||||
|
||||
this.events.onmessage = async event => {
|
||||
const jsonData = JSON.parse(event.data);
|
||||
console.debug(`[SSE] ${event.type}: ${event.data}`);
|
||||
|
||||
switch (event.type) {
|
||||
case 'beat':
|
||||
if (this.beats !== undefined) {
|
||||
this.beats.push(obj);
|
||||
this.beatsEvent.next([obj]); // We just push one, so the map doesn't has to rebuild everything from scratch.
|
||||
this.beats.push(jsonData);
|
||||
this.beatsEvent.next([jsonData]); // We just push one, so the map doesn't has to rebuild everything from scratch.
|
||||
this.beatStats.totalBeats++;
|
||||
}
|
||||
} else if (obj.type === 'phone_available') {
|
||||
this.alert.dynamic(`Device ${obj.displayName} is now online`, obj.severity, 'Device');
|
||||
} else if (obj.type === 'phone_register') {
|
||||
|
||||
console.debug('Received count:', jsonData);
|
||||
break;
|
||||
case 'message':
|
||||
this.alert.info(event.data.message, 'SSE');
|
||||
break;
|
||||
case 'phone_available':
|
||||
this.alert.dynamic(`Device ${jsonData.displayName} is now online`, jsonData.severity, 'Device');
|
||||
break;
|
||||
case 'phone_register':
|
||||
await this.getPhones();
|
||||
this.alert.dynamic(`New device "${obj.displayName}"`, obj.severity, 'New device');
|
||||
} else if (obj.type === 'phone_alive') {
|
||||
this.alert.dynamic('Device is now active', obj.severity, obj.displayName);
|
||||
} else if (obj.type === 'phone_dead') {
|
||||
this.alert.dynamic('Device is now offline', obj.severity, obj.displayName);
|
||||
}
|
||||
this.alert.dynamic(`New device "${jsonData.displayName}"`, jsonData.severity, 'New device');
|
||||
break;
|
||||
case 'phone_alive':
|
||||
this.alert.dynamic('Device is now active', jsonData.severity, jsonData.displayName);
|
||||
break;
|
||||
case 'phone_dead':
|
||||
this.alert.dynamic('Device is now offline', jsonData.severity, jsonData.displayName);
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -171,6 +199,7 @@ export class APIService {
|
||||
*/
|
||||
async login(username: string, password: string): Promise<ILogin> {
|
||||
return new Promise<ILogin>(async (resolve, reject) => {
|
||||
if (this.token !== undefined) reject('User is already logged in.');
|
||||
this.httpClient.post(this.API_ENDPOINT + '/user/login', { username, password }, { responseType: 'json' })
|
||||
.subscribe(async token => {
|
||||
console.log(token);
|
||||
@@ -181,9 +210,9 @@ export class APIService {
|
||||
await this.getUserInfo();
|
||||
await this.getNotifications();
|
||||
|
||||
this.mqttInit();
|
||||
this.subscribeToEvents();
|
||||
|
||||
await this.getBeats();
|
||||
await this.getBeats({ from: moment().startOf('day').unix(), to: moment().unix() });
|
||||
await this.getBeatStats();
|
||||
this.loginEvent.next(true);
|
||||
this.alert.success('Login successful', 'Login', { duration: 2 });
|
||||
@@ -385,7 +414,7 @@ export class APIService {
|
||||
});
|
||||
}
|
||||
|
||||
/* HELPER CLASSES */
|
||||
/* HELPER FUNCTIONS */
|
||||
degreesToRadians(degrees: number): number {
|
||||
return degrees * Math.PI / 180;
|
||||
}
|
||||
@@ -429,7 +458,14 @@ export class APIService {
|
||||
return this.beats[this.beats.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if user is logged in and `false` if not.
|
||||
*/
|
||||
hasSession(): boolean {
|
||||
return this.token !== undefined;
|
||||
}
|
||||
|
||||
getToken(): string {
|
||||
return this.token;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ export class AppComponent implements OnInit{
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
await this.api.login('admin', '$1KDaNCDlyXAOg');
|
||||
this.alert.error('Audio test');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
@@ -14,13 +14,13 @@ import { LoginComponent } from './login/login.component';
|
||||
import { MapComponent } from './map/map.component';
|
||||
import { UserComponent } from './user/user.component';
|
||||
import { DashboardWidgetComponent } from './dashboard-widget/dashboard-widget.component';
|
||||
import { MqttModule } from 'ngx-mqtt';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { environment } from '../environments/environment';
|
||||
import { AdminComponent } from './admin/admin.component';
|
||||
import { AlertComponent } from './_alert/alert/alert.component';
|
||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||
import { NotificationsComponent } from './notifications/notifications.component';
|
||||
import { BackendInterceptor } from './interceptor';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -42,7 +42,6 @@ import { NotificationsComponent } from './notifications/notifications.component'
|
||||
FontAwesomeModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
MqttModule.forRoot({}),
|
||||
NgxMapboxGLModule.withConfig({
|
||||
accessToken: 'pk.eyJ1IjoibW9uZGVpMSIsImEiOiJja2dsY2ZtaG0xZ2o5MnR0ZWs0Mm82OTBpIn0.NzDWN3P6jJLmci_v3MM1tA'
|
||||
}),
|
||||
@@ -50,7 +49,13 @@ import { NotificationsComponent } from './notifications/notifications.component'
|
||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
||||
FontAwesomeModule
|
||||
],
|
||||
providers: [],
|
||||
providers: [
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: BackendInterceptor,
|
||||
multi: true
|
||||
}
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
|
||||
19
frontend/src/app/interceptor.ts
Normal file
19
frontend/src/app/interceptor.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpInterceptor, HttpEvent, HttpResponse, HttpRequest, HttpHandler } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { APIService } from './api.service';
|
||||
|
||||
@Injectable()
|
||||
export class BackendInterceptor implements HttpInterceptor {
|
||||
|
||||
constructor (private api: APIService) {}
|
||||
|
||||
intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
if (this.api.hasSession()) {
|
||||
console.debug('Inject token for', httpRequest.url);
|
||||
return next.handle(httpRequest.clone({ setHeaders: { token: this.api.getToken() } }));
|
||||
}
|
||||
|
||||
return next.handle(httpRequest);
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ addEventListener('message', ({ data }) => {
|
||||
const isNearPoint = isPointInRadius(
|
||||
{ lat: beat2.coordinate[0], lng: beat2.coordinate[1] },
|
||||
{ lat: beat.coordinate[0], lng: beat.coordinate[1] },
|
||||
0.025
|
||||
0.005
|
||||
);
|
||||
|
||||
if (isNearPoint) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<mgl-map [style]="'mapbox://styles/mapbox/dark-v10'" [zoom]="[15]" [center]="[this.lastLocation[0], this.lastLocation[1]]" *ngIf="showMap">
|
||||
<mgl-map [style]="'mapbox://styles/mapbox/outdoors-v11'" [zoom]="[15]" [center]="[this.lastLocation[0], this.lastLocation[1]]" *ngIf="showMap">
|
||||
<mgl-geojson-source id="locHistory" [data]="data"></mgl-geojson-source>
|
||||
<mgl-geojson-source id="locHistoryFiltered" [data]="mostVisitData"></mgl-geojson-source>
|
||||
<mgl-geojson-source id="lastLoc" [data]="lastLocationData"></mgl-geojson-source>
|
||||
|
||||
165
logo2.svg
Normal file
165
logo2.svg
Normal file
@@ -0,0 +1,165 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="512"
|
||||
height="512"
|
||||
viewBox="0 0 135.46666 135.46667"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
|
||||
sodipodi:docname="logo2.svg">
|
||||
<defs
|
||||
id="defs2">
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient894">
|
||||
<stop
|
||||
style="stop-color:#ea0202;stop-opacity:0.21047072"
|
||||
offset="0"
|
||||
id="stop890" />
|
||||
<stop
|
||||
style="stop-color:#ea0202;stop-opacity:0"
|
||||
offset="1"
|
||||
id="stop892" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient882">
|
||||
<stop
|
||||
style="stop-color:#ea0202;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop878" />
|
||||
<stop
|
||||
style="stop-color:#ea0202;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop880" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
id="linearGradient866">
|
||||
<stop
|
||||
style="stop-color:#ea0202;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop862" />
|
||||
<stop
|
||||
style="stop-color:#ea0202;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop864" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient866"
|
||||
id="linearGradient868"
|
||||
x1="12.343376"
|
||||
y1="22.496964"
|
||||
x2="12.343376"
|
||||
y2="-0.6001001"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient882"
|
||||
id="linearGradient884"
|
||||
x1="16"
|
||||
y1="8"
|
||||
x2="6.598474"
|
||||
y2="17.448318"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient894"
|
||||
id="linearGradient888"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="21.759228"
|
||||
y1="2.2183397"
|
||||
x2="0.69895685"
|
||||
y2="21.501596"
|
||||
gradientTransform="matrix(0.80230116,0,0,0.80230116,2.3723861,2.3723861)" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient894"
|
||||
id="linearGradient898"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.52333779,0,0,0.52333779,-18.280053,5.7199465)"
|
||||
x1="21.759228"
|
||||
y1="2.2183397"
|
||||
x2="0.69895685"
|
||||
y2="21.501596" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#292929"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="1.4"
|
||||
inkscape:cx="198.97429"
|
||||
inkscape:cy="322.59346"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="g860"
|
||||
inkscape:document-rotation="0"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1027"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Ebene 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<g
|
||||
style="fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="g860"
|
||||
transform="scale(5.6666667)">
|
||||
<path
|
||||
stroke="none"
|
||||
d="M 0,0 H 24 V 24 H 0 Z"
|
||||
fill="none"
|
||||
id="path843" />
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="9"
|
||||
id="circle845"
|
||||
style="stroke:url(#linearGradient868);stroke-opacity:1;stroke-width:0.88235294;stroke-miterlimit:4;stroke-dasharray:none" />
|
||||
<path
|
||||
d="M 12,17 11,13 7,12 16,8 Z"
|
||||
id="path847"
|
||||
style="stroke:url(#linearGradient884);stroke-opacity:1;stroke-width:0.88235294;stroke-miterlimit:4;stroke-dasharray:none;stroke-linecap:butt" />
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="7.2207103"
|
||||
id="circle886"
|
||||
style="stroke:url(#linearGradient888);stroke-width:0.707913;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
cx="-12"
|
||||
cy="12"
|
||||
r="4.7100401"
|
||||
id="circle896"
|
||||
style="stroke:url(#linearGradient898);stroke-width:0.461769;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="rotate(-90)" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
256
package-lock.json
generated
256
package-lock.json
generated
@@ -2,15 +2,271 @@
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"@types/bson": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.3.tgz",
|
||||
"integrity": "sha512-mVRvYnTOZJz3ccpxhr3wgxVmSeiYinW+zlzQz3SXWaJmD1DuL05Jeq7nKw3SnbKmbleW5qrLG5vdyWe/A9sXhw==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/mongodb": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.3.tgz",
|
||||
"integrity": "sha512-6YNqGP1hk5bjUFaim+QoFFuI61WjHiHE1BNeB41TA00Xd2K7zG4lcWyLLq/XtIp36uMavvS5hoAUJ+1u/GcX2Q==",
|
||||
"requires": {
|
||||
"@types/bson": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "14.14.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.16.tgz",
|
||||
"integrity": "sha512-naXYePhweTi+BMv11TgioE2/FXU4fSl29HAH1ffxVciNsH3rYXjNP2yM8wqmSm7jS20gM8TIklKiTen+1iVncw=="
|
||||
},
|
||||
"angular-font-awesome": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/angular-font-awesome/-/angular-font-awesome-3.1.2.tgz",
|
||||
"integrity": "sha1-k3hzJhLY6MceDXwvqg+t3H+Fjsk="
|
||||
},
|
||||
"bl": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz",
|
||||
"integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==",
|
||||
"requires": {
|
||||
"readable-stream": "^2.3.5",
|
||||
"safe-buffer": "^5.1.1"
|
||||
}
|
||||
},
|
||||
"bluebird": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
|
||||
"integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA=="
|
||||
},
|
||||
"bson": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz",
|
||||
"integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg=="
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
}
|
||||
}
|
||||
},
|
||||
"denque": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz",
|
||||
"integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ=="
|
||||
},
|
||||
"font-awesome": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
|
||||
"integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM="
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
},
|
||||
"kareem": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz",
|
||||
"integrity": "sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ=="
|
||||
},
|
||||
"memory-pager": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
|
||||
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
|
||||
"optional": true
|
||||
},
|
||||
"mongodb": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.3.tgz",
|
||||
"integrity": "sha512-rOZuR0QkodZiM+UbQE5kDsJykBqWi0CL4Ec2i1nrGrUI3KO11r6Fbxskqmq3JK2NH7aW4dcccBuUujAP0ERl5w==",
|
||||
"requires": {
|
||||
"bl": "^2.2.1",
|
||||
"bson": "^1.1.4",
|
||||
"denque": "^1.4.1",
|
||||
"require_optional": "^1.0.1",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"saslprep": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"mongoose": {
|
||||
"version": "5.11.8",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.11.8.tgz",
|
||||
"integrity": "sha512-RRfrYLg7pyuyx7xu5hwadjIZZJB9W2jqIMkL1CkTmk/uOCX3MX2tl4BVIi2rJUtgMNwn6dy3wBD3soB8I9Nlog==",
|
||||
"requires": {
|
||||
"@types/mongodb": "^3.5.27",
|
||||
"bson": "^1.1.4",
|
||||
"kareem": "2.3.2",
|
||||
"mongodb": "3.6.3",
|
||||
"mongoose-legacy-pluralize": "1.0.2",
|
||||
"mpath": "0.8.1",
|
||||
"mquery": "3.2.3",
|
||||
"ms": "2.1.2",
|
||||
"regexp-clone": "1.0.0",
|
||||
"safe-buffer": "5.2.1",
|
||||
"sift": "7.0.1",
|
||||
"sliced": "1.0.1"
|
||||
}
|
||||
},
|
||||
"mongoose-legacy-pluralize": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz",
|
||||
"integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ=="
|
||||
},
|
||||
"mpath": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.1.tgz",
|
||||
"integrity": "sha512-norEinle9aFc05McBawVPwqgFZ7npkts9yu17ztIVLwPwO9rq0OTp89kGVTqvv5rNLMz96E5iWHpVORjI411vA=="
|
||||
},
|
||||
"mquery": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.3.tgz",
|
||||
"integrity": "sha512-cIfbP4TyMYX+SkaQ2MntD+F2XbqaBHUYWk3j+kqdDztPWok3tgyssOZxMHMtzbV1w9DaSlvEea0Iocuro41A4g==",
|
||||
"requires": {
|
||||
"bluebird": "3.5.1",
|
||||
"debug": "3.1.0",
|
||||
"regexp-clone": "^1.0.0",
|
||||
"safe-buffer": "5.1.2",
|
||||
"sliced": "1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"regexp-clone": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz",
|
||||
"integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw=="
|
||||
},
|
||||
"require_optional": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
|
||||
"integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
|
||||
"requires": {
|
||||
"resolve-from": "^2.0.0",
|
||||
"semver": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"resolve-from": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
|
||||
"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||
},
|
||||
"saslprep": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
|
||||
"integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"sparse-bitfield": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
},
|
||||
"sift": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sift/-/sift-7.0.1.tgz",
|
||||
"integrity": "sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g=="
|
||||
},
|
||||
"sliced": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
|
||||
"integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E="
|
||||
},
|
||||
"sparse-bitfield": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
|
||||
"integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"memory-pager": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user