Compare commits
1 Commits
master
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
daa7209742
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -107,3 +107,4 @@ dist
|
|||||||
# Stores VSCode versions used for testing VSCode extensions
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
.vscode-test
|
.vscode-test
|
||||||
|
|
||||||
|
android_new
|
||||||
@@ -44,7 +44,8 @@ dependencies {
|
|||||||
implementation 'com.squareup.moshi:moshi:1.11.0'
|
implementation 'com.squareup.moshi:moshi:1.11.0'
|
||||||
implementation 'com.squareup.moshi:moshi-kotlin:1.11.0'
|
implementation 'com.squareup.moshi:moshi-kotlin:1.11.0'
|
||||||
implementation 'com.rabbitmq:amqp-client:5.9.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.+'
|
testImplementation 'junit:junit:4.+'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
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.view.View
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@@ -31,6 +32,7 @@ import okhttp3.MediaType.Companion.toMediaType
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import java.net.ConnectException
|
||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
|
|
||||||
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
|
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
|
||||||
@@ -49,13 +51,13 @@ class MainActivity : AppCompatActivity() {
|
|||||||
if (TOKEN == "") return;
|
if (TOKEN == "") return;
|
||||||
Thread(Runnable {
|
Thread(Runnable {
|
||||||
val androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
|
val androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
|
||||||
val client = OkHttpClient()
|
/*val client = OkHttpClient()
|
||||||
val req = Request.Builder()
|
val req = Request.Builder()
|
||||||
.url("$API_URL/phone/$androidId")
|
.url("$API_URL/phone/$androidId")
|
||||||
.header("token", TOKEN)
|
.header("token", TOKEN)
|
||||||
.get()
|
.get()
|
||||||
.build()
|
.build()*/
|
||||||
val response = client.newCall(req).execute()
|
val response = HttpRequests.get("$API_URL/phone/$androidId")
|
||||||
|
|
||||||
if (response.code != 200) {
|
if (response.code != 200) {
|
||||||
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Device isn't registered yet. Registering ...", Snackbar.LENGTH_SHORT)
|
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 phoneToJson = moshi.adapter(Phone::class.java)
|
||||||
val json = phoneToJson.toJson(phone)
|
val json = phoneToJson.toJson(phone)
|
||||||
|
|
||||||
val createPhone = Request.Builder()
|
/*val createPhone = Request.Builder()
|
||||||
.url("$API_URL/phone")
|
.url("$API_URL/phone")
|
||||||
.post(
|
.post(
|
||||||
(json).toRequestBody()
|
(json).toRequestBody()
|
||||||
@@ -83,7 +85,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.header("token", TOKEN)
|
.header("token", TOKEN)
|
||||||
.build()
|
.build()
|
||||||
client.newCall(createPhone).execute()
|
client.newCall(createPhone).execute()*/
|
||||||
|
HttpRequests.post("$API_URL/phone", json);
|
||||||
}
|
}
|
||||||
}).start()
|
}).start()
|
||||||
}
|
}
|
||||||
@@ -95,7 +98,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
setSupportActionBar(findViewById(R.id.toolbar))
|
setSupportActionBar(findViewById(R.id.toolbar))
|
||||||
|
|
||||||
val process = Runtime.getRuntime().exec("su")
|
//val process = Runtime.getRuntime().exec("su")
|
||||||
|
|
||||||
// Check authorization
|
// Check authorization
|
||||||
val backendChecks = Thread(Runnable {
|
val backendChecks = Thread(Runnable {
|
||||||
@@ -104,15 +107,40 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
|
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
|
||||||
val jsonToLogin = moshi.adapter(Login::class.java)
|
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 client = OkHttpClient()
|
||||||
val req = Request.Builder()
|
val req = Request.Builder()
|
||||||
.url("$API_URL/user/login")
|
.url("$API_URL/user/login")
|
||||||
.post(
|
.post(
|
||||||
("{ \"username\": \"" + username + "\"," +
|
(token).toRequestBody()
|
||||||
"\"password\": \"" + password + "\" }").toRequestBody()
|
|
||||||
)
|
)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.build()
|
.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 loginResponse = client.newCall(req).execute()
|
||||||
val responseBody = loginResponse.body!!.string()
|
val responseBody = loginResponse.body!!.string()
|
||||||
|
|
||||||
@@ -141,8 +169,10 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val userInfoResponseBody = userinfoResponse.body!!.string()
|
val userInfoResponseBody = userinfoResponse.body!!.string()
|
||||||
USER = jsonToUser.fromJson(userInfoResponseBody)
|
USER = jsonToUser.fromJson(userInfoResponseBody)
|
||||||
|
|
||||||
|
val intent = Intent(this, TrackerService::class.java)
|
||||||
|
|
||||||
// Only start service if authentication went good.
|
// 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)
|
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Login succeeded", Snackbar.LENGTH_SHORT)
|
||||||
.setBackgroundTint(Color.GREEN)
|
.setBackgroundTint(Color.GREEN)
|
||||||
@@ -171,17 +201,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
this.broadcastReceiver = object : BroadcastReceiver() {
|
this.broadcastReceiver = object : BroadcastReceiver() {
|
||||||
@SuppressLint("CutPasteId")
|
@SuppressLint("CutPasteId")
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
val statusRabbit = intent.getBooleanExtra("statusRabbit", false)
|
|
||||||
val statusHttp = intent.getIntExtra("statusHttp", 404)
|
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) {
|
/*if (statusHttp == 200) {
|
||||||
findViewById<TextView>(R.id.httpStatus).text = "ONLINE (no login)"
|
findViewById<TextView>(R.id.httpStatus).text = "ONLINE (no login)"
|
||||||
findViewById<TextView>(R.id.httpStatus).setTextColor(Color.CYAN)
|
findViewById<TextView>(R.id.httpStatus).setTextColor(Color.CYAN)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class User(
|
|||||||
val type: String,
|
val type: String,
|
||||||
val lastLogin: String,
|
val lastLogin: String,
|
||||||
val twoFASecret: String?,
|
val twoFASecret: String?,
|
||||||
val brokerToken: String,
|
val eventToken: String,
|
||||||
val createdAt: String
|
val createdAt: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import android.content.pm.PackageManager
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import android.os.BatteryManager
|
import android.os.BatteryManager
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -18,14 +17,9 @@ import androidx.core.app.ActivityCompat
|
|||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import com.rabbitmq.client.Channel
|
import com.rabbitmq.client.Channel
|
||||||
import com.rabbitmq.client.Connection
|
import com.rabbitmq.client.Connection
|
||||||
import com.rabbitmq.client.ConnectionFactory
|
|
||||||
import com.squareup.moshi.JsonAdapter
|
import com.squareup.moshi.JsonAdapter
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
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() {
|
class TrackerService : Service() {
|
||||||
|
|
||||||
@@ -36,38 +30,9 @@ class TrackerService : Service() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun connectWithBroker() {
|
@SuppressLint("CheckResult")
|
||||||
// This thread only connects to RabbitMQ
|
private fun subscribeToEvents() {
|
||||||
val connectionThread = Thread(Runnable {
|
HttpRequests.sse("http://192.168.178.26/user/events?token=${MainActivity.USER?.eventToken}")
|
||||||
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("HardwareIds")
|
@SuppressLint("HardwareIds")
|
||||||
@@ -105,7 +70,7 @@ class TrackerService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connectWithBroker()
|
subscribeToEvents()
|
||||||
startForeground()
|
startForeground()
|
||||||
return super.onStartCommand(intent, flags, startId)
|
return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,31 +68,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="horizontal">
|
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>
|
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
android:layout_width="match_parent"
|
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 * as mongoose from 'mongoose';
|
||||||
import { exit } from 'process';
|
import { exit } from 'process';
|
||||||
import * as winston from 'winston';
|
import * as winston from 'winston';
|
||||||
|
|
||||||
import { config } from './config';
|
import { config } from './config';
|
||||||
import { GetBeat, GetBeatStats } from './endpoints/beat';
|
import { GetBeat, GetBeatStats } from './endpoints/beat';
|
||||||
import { getNotification } from './endpoints/notification';
|
import { getNotification } from './endpoints/notification';
|
||||||
import { GetPhone, PostPhone } from './endpoints/phone';
|
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 { hashPassword, randomPepper, randomString } from './lib/crypto';
|
||||||
|
import { EventManager } from './lib/eventManager';
|
||||||
import { RabbitMQ } from './lib/rabbit';
|
import { RabbitMQ } from './lib/rabbit';
|
||||||
import { UserType } from './models/user/user.interface';
|
import { UserType } from './models/user/user.interface';
|
||||||
import { User } from './models/user/user.model';
|
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 logger: winston.Logger;
|
||||||
export let rabbitmq: RabbitMQ;
|
export let rabbitmq: RabbitMQ;
|
||||||
|
export let eventManager: EventManager = new EventManager();
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
const { combine, timestamp, label, printf, prettyPrint } = winston.format;
|
const { combine, timestamp, label, printf, prettyPrint } = winston.format;
|
||||||
@@ -108,7 +109,7 @@ async function run() {
|
|||||||
await User.create({
|
await User.create({
|
||||||
name: 'admin',
|
name: 'admin',
|
||||||
password: await hashPassword(randomPassword + salt + randomPepper()),
|
password: await hashPassword(randomPassword + salt + randomPepper()),
|
||||||
brokerToken: randomString(16),
|
eventToken: randomString(16),
|
||||||
salt,
|
salt,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
lastLogin: 0,
|
lastLogin: 0,
|
||||||
@@ -139,15 +140,12 @@ async function run() {
|
|||||||
|
|
||||||
app.get('/', (req, res) => res.status(200).send('OK'));
|
app.get('/', (req, res) => res.status(200).send('OK'));
|
||||||
|
|
||||||
// User authentication
|
// User authentication & actions
|
||||||
app.post('/user/login', (req, res) => LoginUser(req, res));
|
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
|
// CRUD user
|
||||||
app.get('/user/notification', MW_User, (req, res) => getNotification(req, res)); // Notifications
|
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.get('/user/', MW_User, (req, res) => GetUser(req, res));
|
||||||
app.post('/user/', MW_User, (req, res) => PostUser(req, res));
|
app.post('/user/', MW_User, (req, res) => PostUser(req, res));
|
||||||
app.get('/user/:id', MW_User, (req, res) => GetUser(req, res));
|
app.get('/user/:id', MW_User, (req, res) => GetUser(req, res));
|
||||||
@@ -171,8 +169,8 @@ async function run() {
|
|||||||
* Message broker
|
* Message broker
|
||||||
*/
|
*/
|
||||||
rabbitmq = new RabbitMQ();
|
rabbitmq = new RabbitMQ();
|
||||||
await rabbitmq.init();
|
//await rabbitmq.init();
|
||||||
logger.info("Connected with message broker.");
|
//logger.info("Connected with message broker.");
|
||||||
}
|
}
|
||||||
|
|
||||||
run();
|
run();
|
||||||
@@ -16,6 +16,10 @@ export const config: IConfig = {
|
|||||||
host: "0.0.0.0"
|
host: "0.0.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* END OF CONFIG
|
||||||
|
* ====================
|
||||||
|
*/
|
||||||
|
|
||||||
export interface IConfig {
|
export interface IConfig {
|
||||||
authentification: {
|
authentification: {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { Response } from "express";
|
import { Response } from "express";
|
||||||
|
import { eventManager, logger } from "../app";
|
||||||
import { LivebeatRequest } from "../lib/request";
|
import { LivebeatRequest } from "../lib/request";
|
||||||
import { IBeat } from "../models/beat/beat.interface";
|
import { IBeat } from "../models/beat/beat.interface";
|
||||||
import { Beat } from "../models/beat/beat.model.";
|
import { Beat } from "../models/beat/beat.model.";
|
||||||
|
import { ISeverity } from "../models/notifications/notification.interface";
|
||||||
import { Phone } from "../models/phone/phone.model";
|
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) {
|
export async function GetBeatStats(req: LivebeatRequest, res: Response) {
|
||||||
const phones = await Phone.find({ user: req.user?._id });
|
const phones = await Phone.find({ user: req.user?._id });
|
||||||
const perPhone: any = {};
|
const perPhone: any = {};
|
||||||
@@ -20,23 +24,26 @@ export async function GetBeatStats(req: LivebeatRequest, res: Response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function GetBeat(req: LivebeatRequest, res: Response) {
|
export async function GetBeat(req: LivebeatRequest, res: Response) {
|
||||||
const from: number = Number(req.query.from);
|
const from: number = Number(req.query.from || 0);
|
||||||
const to: number = Number(req.query.to);
|
const to: number = Number(req.query.to || Date.now() / 1000);
|
||||||
const limit: number = Number(req.query.limit || 10000);
|
const limit: number = Number(req.query.limit || 10000);
|
||||||
const sort: number = Number(req.query.sort || 1); // Either -1 or 1
|
const sort: number = Number(req.query.sort || 1); // Either -1 or 1
|
||||||
const phoneId = req.query.phoneId;
|
const phoneId = req.query.phoneId;
|
||||||
|
|
||||||
// Grab default phone if non was provided.
|
// 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 });
|
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) {
|
if (phone !== null) {
|
||||||
beats = await Beat.find(
|
beats = await Beat.find(
|
||||||
{
|
{
|
||||||
phone: phone._id,
|
phone: phone._id,
|
||||||
createdAt: {
|
createdAt: {
|
||||||
$gte: new Date((from | 0) * 1000),
|
$gte: new Date((from)),
|
||||||
$lte: new Date((to | Date.now() /1000) * 1000)
|
$lte: new Date(to * 1000)
|
||||||
}
|
}
|
||||||
}).sort({ _id: sort }).limit(limit);
|
}).sort({ _id: sort }).limit(limit);
|
||||||
res.status(200).send(beats);
|
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' });
|
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";
|
import { Phone } from "../models/phone/phone.model";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function GetPhone(req: LivebeatRequest, res: Response) {
|
export async function GetPhone(req: LivebeatRequest, res: Response) {
|
||||||
const phoneId: String = req.params['id'];
|
const phoneId: String = req.params['id'];
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { decode, sign, verify } from 'jsonwebtoken';
|
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 * as jwt from 'jsonwebtoken';
|
||||||
import { config } from '../config';
|
import { config } from '../config';
|
||||||
import { hashPassword, randomPepper, randomString, verifyPassword } from '../lib/crypto';
|
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 salt = randomString(config.authentification.salt_length);
|
||||||
const brokerToken = randomString(16);
|
const eventToken = randomString(16);
|
||||||
const hashedPassword = await hashPassword(password + salt + randomPepper()).catch(error => {
|
const hashedPassword = await hashPassword(password + salt + randomPepper()).catch(error => {
|
||||||
res.status(400).send({ message: 'Provided password is too weak and cannot be used.' });
|
res.status(400).send({ message: 'Provided password is too weak and cannot be used.' });
|
||||||
return;
|
return;
|
||||||
@@ -61,7 +61,7 @@ export async function PostUser(req: LivebeatRequest, res: Response) {
|
|||||||
name,
|
name,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
salt,
|
salt,
|
||||||
brokerToken,
|
eventToken,
|
||||||
type,
|
type,
|
||||||
lastLogin: new Date(0)
|
lastLogin: new Date(0)
|
||||||
});
|
});
|
||||||
@@ -72,6 +72,27 @@ export async function PostUser(req: LivebeatRequest, res: Response) {
|
|||||||
res.status(200).send({ setupToken });
|
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) {
|
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 });
|
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.
|
* This middleware validates any tokens that are required to access most of the endpoints.
|
||||||
* Note: This validation doesn't contain any permission checking.
|
* 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);
|
this.timeouts.delete(phone.id);
|
||||||
phone.active = false;
|
phone.active = false;
|
||||||
await phone.save();
|
await phone.save();
|
||||||
}, 30_000);
|
}, 60_000);
|
||||||
this.timeouts.set(phone.id, timeoutTimer);
|
this.timeouts.set(phone.id, timeoutTimer);
|
||||||
|
|
||||||
this.publish(phone.user.toString(), newBeat.toJSON(), 'beat');
|
this.publish(phone.user.toString(), newBeat.toJSON(), 'beat');
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { IPhone } from '../phone/phone.interface';
|
|||||||
export interface IBeat extends Document {
|
export interface IBeat extends Document {
|
||||||
// [latitude, longitude, altitude, accuracy, speed]
|
// [latitude, longitude, altitude, accuracy, speed]
|
||||||
coordinate?: number[],
|
coordinate?: number[],
|
||||||
accuracy: number,
|
accuracy?: number,
|
||||||
speed: number,
|
speed?: number,
|
||||||
battery?: number,
|
battery?: number,
|
||||||
phone: IPhone,
|
phone: IPhone,
|
||||||
createdAt?: Date
|
createdAt?: Date
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ export enum ISeverity {
|
|||||||
ERROR = 3
|
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 {
|
export interface INotification extends Document {
|
||||||
type: NotificationType;
|
type: NotificationType;
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ export interface IUser extends Document {
|
|||||||
type: UserType,
|
type: UserType,
|
||||||
lastLogin: Date,
|
lastLogin: Date,
|
||||||
twoFASecret?: string,
|
twoFASecret?: string,
|
||||||
brokerToken: string,
|
eventToken: string,
|
||||||
createdAt?: Date
|
createdAt?: Date
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ const schemaUser = new Schema({
|
|||||||
salt: { type: String, required: true },
|
salt: { type: String, required: true },
|
||||||
type: { type: String, required: true, default: 'user' }, // This could be user, admin, guest
|
type: { type: String, required: true, default: 'user' }, // This could be user, admin, guest
|
||||||
twoFASecret: { type: String, required: false },
|
twoFASecret: { type: String, required: false },
|
||||||
brokerToken: { type: String, required: true },
|
eventToken: { type: String, required: true },
|
||||||
lastLogin: { type: Date, required: true, default: Date.now },
|
lastLogin: { type: Date, required: true, default: Date.now },
|
||||||
}, {
|
}, {
|
||||||
timestamps: {
|
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",
|
"@types/node": "^14.14.9",
|
||||||
"amqplib": "^0.6.0",
|
"amqplib": "^0.6.0",
|
||||||
"argon2": "^0.27.0",
|
"argon2": "^0.27.0",
|
||||||
|
"bi-directional-map": "^1.0.0",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
@@ -34,20 +35,20 @@
|
|||||||
"winston": "^3.3.3"
|
"winston": "^3.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/amqplib": "0.5.14",
|
||||||
"@types/argon2": "0.15.0",
|
"@types/argon2": "0.15.0",
|
||||||
"@types/body-parser": "1.19.0",
|
"@types/body-parser": "1.19.0",
|
||||||
"@types/chalk": "2.2.0",
|
"@types/chalk": "2.2.0",
|
||||||
|
"@types/cors": "2.8.8",
|
||||||
"@types/dotenv": "8.2.0",
|
"@types/dotenv": "8.2.0",
|
||||||
"@types/express": "4.17.8",
|
"@types/express": "4.17.8",
|
||||||
"@types/figlet": "1.2.0",
|
"@types/figlet": "1.2.0",
|
||||||
|
"@types/jsonwebtoken": "8.5.0",
|
||||||
|
"@types/moment": "2.13.0",
|
||||||
"@types/mongoose": "5.7.36",
|
"@types/mongoose": "5.7.36",
|
||||||
"@types/typescript": "2.0.0",
|
"@types/typescript": "2.0.0",
|
||||||
"@types/winston": "2.4.4",
|
"@types/winston": "2.4.4",
|
||||||
"concurrently": "^5.3.0",
|
"concurrently": "^5.3.0",
|
||||||
"nodemon": "^2.0.5",
|
"nodemon": "^2.0.5"
|
||||||
"@types/jsonwebtoken": "8.5.0",
|
|
||||||
"@types/amqplib": "0.5.14",
|
|
||||||
"@types/cors": "2.8.8",
|
|
||||||
"@types/moment": "2.13.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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",
|
"moment": "^2.29.1",
|
||||||
"ng2-charts": "^2.4.2",
|
"ng2-charts": "^2.4.2",
|
||||||
"ngx-mapbox-gl": "^4.8.1",
|
"ngx-mapbox-gl": "^4.8.1",
|
||||||
"ngx-mqtt": "^7.0.14",
|
|
||||||
"rxjs": "~6.6.0",
|
"rxjs": "~6.6.0",
|
||||||
"tslib": "^2.0.0",
|
"tslib": "^2.0.0",
|
||||||
"zone.js": "~0.10.2"
|
"zone.js": "~0.10.2"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { AfterContentInit, Component, OnDestroy, OnInit } from '@angular/core';
|
import { AfterContentInit, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { resolve } from 'dns';
|
|
||||||
import { APIService, UserType } from '../api.service';
|
import { APIService, UserType } from '../api.service';
|
||||||
import { AlertService } from '../_alert/alert.service';
|
import { AlertService } from '../_alert/alert.service';
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { MqttService } from 'ngx-mqtt';
|
|
||||||
import { BehaviorSubject } from 'rxjs';
|
import { BehaviorSubject } from 'rxjs';
|
||||||
import * as moment from 'moment';
|
import * as moment from 'moment';
|
||||||
import { error } from 'protractor';
|
import { AlertService, AlertType } from './_alert/alert.service';
|
||||||
import { AlertService } from './_alert/alert.service';
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
* ==========================
|
||||||
* DEFINITION OF TYPE
|
* DEFINITION OF TYPE
|
||||||
*/
|
*/
|
||||||
export interface ILogin {
|
export interface ILogin {
|
||||||
@@ -44,7 +43,7 @@ export enum UserType {
|
|||||||
export interface IUser {
|
export interface IUser {
|
||||||
_id: string;
|
_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
brokerToken: string;
|
eventToken: string;
|
||||||
type: UserType;
|
type: UserType;
|
||||||
lastLogin: Date;
|
lastLogin: Date;
|
||||||
twoFASecret?: string;
|
twoFASecret?: string;
|
||||||
@@ -84,8 +83,13 @@ export interface INotification extends Document {
|
|||||||
user: IUser;
|
user: IUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CustomEvent extends Event {
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* END OF THE DEFINITION OF TYPE
|
* END OF THE TYPE DEFINITION
|
||||||
|
* ==========================
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@@ -94,9 +98,9 @@ export interface INotification extends Document {
|
|||||||
export class APIService {
|
export class APIService {
|
||||||
|
|
||||||
private token: string;
|
private token: string;
|
||||||
|
private events: EventSource | undefined;
|
||||||
|
|
||||||
username: string;
|
username: string;
|
||||||
rabbitmq: any;
|
|
||||||
|
|
||||||
// Passthough data (not useful for api but a way for components to share data)
|
// Passthough data (not useful for api but a way for components to share data)
|
||||||
showFilter = true;
|
showFilter = true;
|
||||||
@@ -112,7 +116,7 @@ export class APIService {
|
|||||||
user: IUser = {
|
user: IUser = {
|
||||||
_id: '',
|
_id: '',
|
||||||
name: '',
|
name: '',
|
||||||
brokerToken: '',
|
eventToken: '',
|
||||||
lastLogin: new Date(2020, 3, 1),
|
lastLogin: new Date(2020, 3, 1),
|
||||||
type: UserType.GUEST,
|
type: UserType.GUEST,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -128,42 +132,66 @@ export class APIService {
|
|||||||
|
|
||||||
API_ENDPOINT = 'http://192.168.178.26:8040';
|
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 functions opens a new http connection that stays open, forever, to receive events from the backend.
|
||||||
this.mqtt.connect({
|
*/
|
||||||
hostname: '192.168.178.26',
|
async subscribeToEvents() {
|
||||||
port: 15675,
|
let shownError = false;
|
||||||
protocol: 'ws',
|
|
||||||
path: '/ws',
|
|
||||||
username: this.user.name,
|
|
||||||
password: this.user.brokerToken
|
|
||||||
});
|
|
||||||
|
|
||||||
this.mqtt.observe(this.user._id).subscribe(async message => {
|
// If there is already a event, close it.
|
||||||
if (message !== undefined || message !== null) {
|
if (this.events !== undefined) {
|
||||||
const obj = JSON.parse(message.payload.toString());
|
this.events.close();
|
||||||
console.log('Received message:', obj);
|
}
|
||||||
|
|
||||||
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) {
|
if (this.beats !== undefined) {
|
||||||
this.beats.push(obj);
|
this.beats.push(jsonData);
|
||||||
this.beatsEvent.next([obj]); // We just push one, so the map doesn't has to rebuild everything from scratch.
|
this.beatsEvent.next([jsonData]); // We just push one, so the map doesn't has to rebuild everything from scratch.
|
||||||
this.beatStats.totalBeats++;
|
this.beatStats.totalBeats++;
|
||||||
}
|
}
|
||||||
} else if (obj.type === 'phone_available') {
|
|
||||||
this.alert.dynamic(`Device ${obj.displayName} is now online`, obj.severity, 'Device');
|
console.debug('Received count:', jsonData);
|
||||||
} else if (obj.type === 'phone_register') {
|
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();
|
await this.getPhones();
|
||||||
this.alert.dynamic(`New device "${obj.displayName}"`, obj.severity, 'New device');
|
this.alert.dynamic(`New device "${jsonData.displayName}"`, jsonData.severity, 'New device');
|
||||||
} else if (obj.type === 'phone_alive') {
|
break;
|
||||||
this.alert.dynamic('Device is now active', obj.severity, obj.displayName);
|
case 'phone_alive':
|
||||||
} else if (obj.type === 'phone_dead') {
|
this.alert.dynamic('Device is now active', jsonData.severity, jsonData.displayName);
|
||||||
this.alert.dynamic('Device is now offline', obj.severity, obj.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> {
|
async login(username: string, password: string): Promise<ILogin> {
|
||||||
return new Promise<ILogin>(async (resolve, reject) => {
|
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' })
|
this.httpClient.post(this.API_ENDPOINT + '/user/login', { username, password }, { responseType: 'json' })
|
||||||
.subscribe(async token => {
|
.subscribe(async token => {
|
||||||
console.log(token);
|
console.log(token);
|
||||||
@@ -181,9 +210,9 @@ export class APIService {
|
|||||||
await this.getUserInfo();
|
await this.getUserInfo();
|
||||||
await this.getNotifications();
|
await this.getNotifications();
|
||||||
|
|
||||||
this.mqttInit();
|
this.subscribeToEvents();
|
||||||
|
|
||||||
await this.getBeats();
|
await this.getBeats({ from: moment().startOf('day').unix(), to: moment().unix() });
|
||||||
await this.getBeatStats();
|
await this.getBeatStats();
|
||||||
this.loginEvent.next(true);
|
this.loginEvent.next(true);
|
||||||
this.alert.success('Login successful', 'Login', { duration: 2 });
|
this.alert.success('Login successful', 'Login', { duration: 2 });
|
||||||
@@ -385,7 +414,7 @@ export class APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* HELPER CLASSES */
|
/* HELPER FUNCTIONS */
|
||||||
degreesToRadians(degrees: number): number {
|
degreesToRadians(degrees: number): number {
|
||||||
return degrees * Math.PI / 180;
|
return degrees * Math.PI / 180;
|
||||||
}
|
}
|
||||||
@@ -429,7 +458,14 @@ export class APIService {
|
|||||||
return this.beats[this.beats.length - 1];
|
return this.beats[this.beats.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns `true` if user is logged in and `false` if not.
|
||||||
|
*/
|
||||||
hasSession(): boolean {
|
hasSession(): boolean {
|
||||||
return this.token !== undefined;
|
return this.token !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getToken(): string {
|
||||||
|
return this.token;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ export class AppComponent implements OnInit{
|
|||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
await this.api.login('admin', '$1KDaNCDlyXAOg');
|
await this.api.login('admin', '$1KDaNCDlyXAOg');
|
||||||
this.alert.error('Audio test');
|
|
||||||
return;
|
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 { NgModule } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
@@ -14,13 +14,13 @@ import { LoginComponent } from './login/login.component';
|
|||||||
import { MapComponent } from './map/map.component';
|
import { MapComponent } from './map/map.component';
|
||||||
import { UserComponent } from './user/user.component';
|
import { UserComponent } from './user/user.component';
|
||||||
import { DashboardWidgetComponent } from './dashboard-widget/dashboard-widget.component';
|
import { DashboardWidgetComponent } from './dashboard-widget/dashboard-widget.component';
|
||||||
import { MqttModule } from 'ngx-mqtt';
|
|
||||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
import { AdminComponent } from './admin/admin.component';
|
import { AdminComponent } from './admin/admin.component';
|
||||||
import { AlertComponent } from './_alert/alert/alert.component';
|
import { AlertComponent } from './_alert/alert/alert.component';
|
||||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||||
import { NotificationsComponent } from './notifications/notifications.component';
|
import { NotificationsComponent } from './notifications/notifications.component';
|
||||||
|
import { BackendInterceptor } from './interceptor';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -42,7 +42,6 @@ import { NotificationsComponent } from './notifications/notifications.component'
|
|||||||
FontAwesomeModule,
|
FontAwesomeModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
MqttModule.forRoot({}),
|
|
||||||
NgxMapboxGLModule.withConfig({
|
NgxMapboxGLModule.withConfig({
|
||||||
accessToken: 'pk.eyJ1IjoibW9uZGVpMSIsImEiOiJja2dsY2ZtaG0xZ2o5MnR0ZWs0Mm82OTBpIn0.NzDWN3P6jJLmci_v3MM1tA'
|
accessToken: 'pk.eyJ1IjoibW9uZGVpMSIsImEiOiJja2dsY2ZtaG0xZ2o5MnR0ZWs0Mm82OTBpIn0.NzDWN3P6jJLmci_v3MM1tA'
|
||||||
}),
|
}),
|
||||||
@@ -50,7 +49,13 @@ import { NotificationsComponent } from './notifications/notifications.component'
|
|||||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
||||||
FontAwesomeModule
|
FontAwesomeModule
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: BackendInterceptor,
|
||||||
|
multi: true
|
||||||
|
}
|
||||||
|
],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
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(
|
const isNearPoint = isPointInRadius(
|
||||||
{ lat: beat2.coordinate[0], lng: beat2.coordinate[1] },
|
{ lat: beat2.coordinate[0], lng: beat2.coordinate[1] },
|
||||||
{ lat: beat.coordinate[0], lng: beat.coordinate[1] },
|
{ lat: beat.coordinate[0], lng: beat.coordinate[1] },
|
||||||
0.025
|
0.005
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isNearPoint) {
|
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="locHistory" [data]="data"></mgl-geojson-source>
|
||||||
<mgl-geojson-source id="locHistoryFiltered" [data]="mostVisitData"></mgl-geojson-source>
|
<mgl-geojson-source id="locHistoryFiltered" [data]="mostVisitData"></mgl-geojson-source>
|
||||||
<mgl-geojson-source id="lastLoc" [data]="lastLocationData"></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,
|
"requires": true,
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"dependencies": {
|
"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": {
|
"angular-font-awesome": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/angular-font-awesome/-/angular-font-awesome-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/angular-font-awesome/-/angular-font-awesome-3.1.2.tgz",
|
||||||
"integrity": "sha1-k3hzJhLY6MceDXwvqg+t3H+Fjsk="
|
"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": {
|
"font-awesome": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
|
||||||
"integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM="
|
"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