feature/replace-rabbitmq-with-socketio #8

Open
Nicolas wants to merge 3 commits from feature/replace-rabbitmq-with-socketio into master
72 changed files with 5861 additions and 23719 deletions
Showing only changes of commit 19b7c05d75 - Show all commits

122
android/.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,122 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="1.8" />
<bytecodeTargetLevel target="11" />
</component>
</project>

View File

@@ -4,7 +4,7 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="PLATFORM" />
<option name="testRunner" value="GRADLE" />
<option name="disableWrapperSourceDistributionNotification" value="true" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
@@ -16,7 +16,6 @@
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings>
</option>
</component>

13
android/.idea/misc.xml generated
View File

@@ -1,6 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="app/src/main/res/layout/activity_main.xml" value="0.22291666666666668" />
<entry key="app/src/main/res/layout/activity_setup.xml" value="0.3578125" />
<entry key="app/src/main/res/layout/content_setup.xml" value="0.3578125" />
<entry key="app/src/main/res/layout/input_dialog.xml" value="0.19791666666666666" />
<entry key="app/src/main/res/layout/setup.xml" value="0.33" />
</map>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@@ -21,6 +21,7 @@ android {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
debuggable false
}
}
compileOptions {
@@ -30,10 +31,17 @@ android {
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
viewBinding true
}
}
dependencies {
implementation ('io.socket:socket.io-client:2.0.1') {
// excluding org.json which is provided by Android
exclude group: 'org.json', module: 'json'
}
implementation fileTree(dir: 'libs', include: ['lineage-sdk.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.1.0'
@@ -46,6 +54,6 @@ dependencies {
implementation 'com.rabbitmq:amqp-client:5.9.0'
implementation "com.squareup.okhttp3:okhttp:4.9.0"
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

Binary file not shown.

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="de.nicolasklier.livebeat">
package="de.nicolasklier.livebeat" >
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@@ -19,18 +19,26 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.Livebeat">
android:theme="@style/Theme.Livebeat"
android:usesCleartextTraffic="true" >
<activity
android:name=".SetupActivity"
android:exported="false"
android:label="@string/title_activity_setup"
android:theme="@style/Theme.Livebeat.NoActionBar" />
<service android:name=".TrackerService" />
<receiver android:name=".BootReceiver">
<intent-filter >
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<receiver android:name=".BootReceiver" >
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/Theme.Livebeat.NoActionBar">
android:theme="@style/Theme.Livebeat.NoActionBar" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@@ -10,16 +10,13 @@ import android.content.pm.PackageManager
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.telephony.TelephonyManager
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
@@ -27,11 +24,9 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.logging.Logger
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
class MainActivity : AppCompatActivity() {
@@ -46,6 +41,15 @@ class MainActivity : AppCompatActivity() {
@SuppressLint("HardwareIds")
fun checkIfPhoneIsRegistered() {
val pref = this.getPreferences(Context.MODE_PRIVATE) ?: return
val accessToken = pref.getString("accessToken", "");
// App is not setup
if (accessToken == "") {
val intent = Intent(baseContext, SetupActivity::class.java);
startActivity(intent);
}
if (TOKEN == "") return;
Thread(Runnable {
val androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
@@ -95,8 +99,6 @@ class MainActivity : AppCompatActivity() {
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
val process = Runtime.getRuntime().exec("su")
// Check authorization
val backendChecks = Thread(Runnable {
val username = findViewById<TextView>(R.id.username).text
@@ -142,7 +144,7 @@ class MainActivity : AppCompatActivity() {
USER = jsonToUser.fromJson(userInfoResponseBody)
// Only start service if authentication went good.
startService(Intent(this, TrackerService::class.java))
// startService(Intent(this, TrackerService::class.java))
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Login succeeded", Snackbar.LENGTH_SHORT)
.setBackgroundTint(Color.GREEN)
@@ -174,14 +176,6 @@ class MainActivity : AppCompatActivity() {
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)
@@ -203,6 +197,14 @@ class MainActivity : AppCompatActivity() {
}
}
@ColorInt
private fun getAccentColor(): Int {
val attr = intArrayOf(android.R.attr.colorAccent)
val typedArray = obtainStyledAttributes(android.R.style.Theme_DeviceDefault, attr)
return typedArray.getColor(0, Color.BLACK)
.also { typedArray.recycle() }
}
private fun checkPerms() {
if (ActivityCompat.checkSelfPermission(
this,

View File

@@ -22,12 +22,21 @@ class Phone(
val architecture: String
) {}
class PhoneRegistration (
val phone: Phone,
val token: String
) {}
class PhoneSubmitPairCode (
val phoneId: String,
val code: String
) {}
class User(
val name: String,
val type: String,
val lastLogin: String,
val twoFASecret: String?,
val brokerToken: String,
val createdAt: String
)

View File

@@ -0,0 +1,195 @@
package de.nicolasklier.livebeat
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.view.View
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.ui.AppBarConfiguration
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import de.nicolasklier.livebeat.databinding.ActivitySetupBinding
import de.nicolasklier.livebeat.dialogs.ErrorDialog
import de.nicolasklier.livebeat.dialogs.InputDialog
import io.socket.client.IO
import io.socket.client.Socket
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.lang.Exception
class SetupActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivitySetupBinding
private var host = ""
private var username = ""
private var password = ""
private var token = ""
private lateinit var socket: Socket;
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySetupBinding.inflate(layoutInflater)
setContentView(binding.root)
findViewById<Button>(R.id.btnLogin).setOnClickListener {
kotlin.run {
host = findViewById<EditText>(R.id.inputServerIp).text.toString()
username = findViewById<EditText>(R.id.inputUsername).text.toString()
password = findViewById<EditText>(R.id.inputPassword).text.toString()
findViewById<LinearLayout>(R.id.connectLayout).visibility = View.VISIBLE;
tryConnect()
}
}
}
private fun updateStatus(text: String) {
runOnUiThread {
findViewById<TextView>(R.id.connectStatus).text = text;
}
}
private fun throwError(text: String) {
val dialog = ErrorDialog(text)
dialog.show(supportFragmentManager, "");
runOnUiThread {
findViewById<LinearLayout>(R.id.connectLayout).visibility = View.GONE;
}
}
private fun tryConnect() {
updateStatus("Connect to backend");
// Connection values
val options = IO.Options.builder()
.setTransports(arrayOf("websocket"))
.build();
try {
socket = IO.socket("http://$host:8040", options)
socket.connect()
} catch (e: Exception) {
updateStatus("Failed to connect");
Log.e("Socket.io", "Unable to connect to socket: ${e.message}")
return;
}
socket.on("connect") { args ->
run {
updateStatus("Connected")
login();
}
}
}
private fun login() {
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val jsonToLogin = moshi.adapter(Login::class.java)
val client = OkHttpClient()
val req = Request.Builder()
.url("${MainActivity.API_URL}/user/login")
.post(
("{ \"username\": \"" + username + "\"," +
"\"password\": \"" + password + "\" }").toRequestBody()
)
.header("Content-Type", "application/json")
.build()
val loginResponse = client.newCall(req).execute()
val responseBody = loginResponse.body!!.string()
if (loginResponse.code == 200) {
token = jsonToLogin.fromJson(responseBody)!!.token
requestAccess()
} else {
throwError("Username or password is wrong.")
}
}
private fun requestAccess() {
var phoneId = "";
val androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
val phone = Phone(
androidId,
Build.MODEL,
Build.PRODUCT,
Build.VERSION.BASE_OS + " " + Build.VERSION.RELEASE + " " + Build.VERSION.CODENAME,
System.getProperty("os.arch")
)
val phoneRegistration = PhoneRegistration(
phone,
token
)
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val phoneToJson = moshi.adapter(PhoneRegistration::class.java)
val json = phoneToJson.toJson(phoneRegistration)
socket.emit("requestAccess", json)
updateStatus("Await new phone id")
// We received a response from the backend containing the phone id.
socket.on("requestAccess") { args ->
run {
phoneId = (args[0] as JSONObject).getString("phoneId");
updateStatus("Await user to enter pair code")
fun promptUser() {
val dialog = InputDialog("Pair code" , "Look into your device overview to get the pair code.") { choice, code ->
if (choice == InputDialog.UserChoice.CANCEL) {
updateStatus("Process has been canceled by user.")
return@InputDialog;
}
updateStatus("Validate code $code for $phoneId")
val submitCode = PhoneSubmitPairCode(
phoneId,
code
);
val submitCodeToJson = moshi.adapter(PhoneSubmitPairCode::class.java)
val submitCodeJson = submitCodeToJson.toJson(submitCode)
socket.emit("submitPairCode", submitCodeJson);
}
dialog.show(supportFragmentManager, "");
}
// The backend only calls this event again if the code is incorrect. Otherwise `accessGranted` is called.
socket.on("submitPairCode") { args ->
run {
if ((args[0] as String) == "") {
updateStatus("Code has been incorrect")
// Prompt user to enter code as long as it's invalid
promptUser()
} else {
// If response is not empty, we received a token (let's just hope that)
updateStatus("Code has been correct")
val sharedPref = this.getPreferences(Context.MODE_PRIVATE) ?: return@on
with (sharedPref.edit()) {
putString("accessToken", args[0] as String)
commit()
}
// Return to previous activity.
this.finish()
}
}
}
promptUser()
}
}
}
}

View File

@@ -16,19 +16,27 @@ import android.provider.Settings
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.VISIBILITY_PRIVATE
import androidx.core.app.NotificationCompat.VISIBILITY_SECRET
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 io.socket.client.IO
import io.socket.client.Socket
import io.socket.emitter.Emitter
import okhttp3.EventListener
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.IOException
import java.lang.Exception
import java.util.concurrent.TimeoutException
class TrackerService : Service() {
var isSocketConnected = false;
val conn = arrayOfNulls<Connection>(1)
val channel = arrayOfNulls<Channel>(1)
@@ -36,35 +44,47 @@ class TrackerService : Service() {
return null
}
private fun connectWithBroker() {
private fun connectSocket() {
// This thread only connects to RabbitMQ
val connectionThread = Thread(Runnable {
val client = OkHttpClient()
val socket: Socket;
// Connection values
val options = IO.Options.builder()
.setTransports(arrayOf("websocket"))
.build();
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()
socket = IO.socket("http://192.168.178.26:8040", options)
socket.connect()
} catch (e: Exception) {
Log.e("Socket.io", "Unable to connect to socket: ${e.message}")
return@Runnable
}
val intent = Intent("de.nicolasklier.livebeat")
val bundle = Bundle()
bundle.putBoolean("statusRabbit", true)
intent.putExtras(bundle)
this.sendBroadcast(intent)
socket.on("test") { args ->
run {
Log.i("Socket.io", args[0].toString());
}
}
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()
socket.on("connect") { args ->
run {
isSocketConnected = true;
}
}
socket.on("disconnect") { args ->
run {
isSocketConnected = false;
}
}
if (socket.connected()) {
socket.emit("test", "This is a message from my phone!")
Log.i("Socket.io", "Published test message")
} else {
Log.e("Socket.io", "Cannot send test message because I'm not connected with the Socket.io server.")
}
})
connectionThread.start()
@@ -105,13 +125,15 @@ class TrackerService : Service() {
}
}
connectWithBroker()
startForeground()
connectSocket()
start()
return super.onStartCommand(intent, flags, startId)
}
private fun startForeground() {
val noticicationIntent = Intent(this, MainActivity::class.java)
private fun start() {
val noticicationIntent = Intent(this, MainActivity::class.java).apply {
action = "Stop"
}
val pendingIntent = PendingIntent.getActivity(this, 0, noticicationIntent, 0)
val chan = NotificationChannel(
NOTIF_CHANNEL_ID,
@@ -121,17 +143,24 @@ class TrackerService : Service() {
chan.lockscreenVisibility = Notification.VISIBILITY_SECRET
val manager = (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
manager.createNotificationChannel(chan)
startForeground(NOTIF_ID, NotificationCompat.Builder(this, NOTIF_CHANNEL_ID)
val notification = NotificationCompat.Builder(this, NOTIF_CHANNEL_ID)
.setOngoing(true)
.setContentTitle("Livebeat")
.setContentText("Tracker is running")
.setContentIntent(pendingIntent)
.setCategory(Notification.CATEGORY_SERVICE)
.setPriority(NotificationManager.IMPORTANCE_LOW)
.setSmallIcon(R.mipmap.ic_launcher)
.setChannelId(NOTIF_CHANNEL_ID)
.setColorized(true)
.setShowWhen(false)
.setVisibility(VISIBILITY_SECRET)
.setColor(Color.BLACK)
.build())
.addAction(R.drawable.ic_launcher_background, "Stop", pendingIntent)
.build()
manager.notify(0, notification)
}
companion object {

View File

@@ -0,0 +1,23 @@
package de.nicolasklier.livebeat.dialogs
import android.app.AlertDialog
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
class ErrorDialog(val message: String) : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
val builder = AlertDialog.Builder(it)
builder.setMessage(message)
.setPositiveButton("Ok"
) { dialog, _ ->
dialog.cancel()
}
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
}

View File

@@ -0,0 +1,43 @@
package de.nicolasklier.livebeat.dialogs
import android.app.AlertDialog
import android.app.Dialog
import android.os.Bundle
import android.widget.EditText
import androidx.fragment.app.DialogFragment
import de.nicolasklier.livebeat.R
import de.nicolasklier.livebeat.User
class InputDialog(val title: String, val message: String, val callback: (UserChoice, String) -> Unit) : DialogFragment() {
enum class UserChoice {
CANCEL,
SUBMIT
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
val inflater = requireActivity().layoutInflater;
val view = inflater.inflate(R.layout.input_dialog, null);
val builder = AlertDialog.Builder(it)
builder
.setMessage(message)
.setTitle(title)
.setCancelable(false)
.setView(view)
.setNegativeButton("Cancel"
) { dialog, _ ->
callback(UserChoice.CANCEL, "")
dialog.cancel()
}
.setPositiveButton("Submit"
) { dialog, _ ->
callback(UserChoice.SUBMIT, view.findViewById<EditText>(R.id.input).text.toString());
}
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
}

View File

@@ -64,36 +64,6 @@
</LinearLayout>
<LinearLayout
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>
<Space
android:layout_width="match_parent"
android:layout_height="120dp" />

View File

@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SetupActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="176dp"
android:layout_marginTop="51dp"
android:layout_marginEnd="177dp"
android:text="Connect"
android:textColor="@color/white"
android:textSize="34sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/linearLayout2"
android:layout_width="331dp"
android:layout_height="wrap_content"
android:layout_marginTop="200dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<EditText
android:id="@+id/inputServerIp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="Server IP"
android:inputType="textPersonName"
android:text="192.168.178.26" />
<EditText
android:id="@+id/inputUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="Username"
android:inputType="textPersonName"
android:text="admin"
android:textColor="@color/white" />
<EditText
android:id="@+id/inputPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="Password"
android:inputType="textPassword"
android:text="$1KDaNCDlyXAOg"
android:textColor="@color/white" />
</LinearLayout>
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="174dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="179dp"
android:text="You'll need to login to a Livebeat server instance."
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.489"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
<Button
android:id="@+id/btnLogin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="161dp"
android:layout_marginTop="85dp"
android:layout_marginEnd="162dp"
android:text="Login"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linearLayout2" />
<LinearLayout
android:id="@+id/connectLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnLogin">
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="181dp"
android:layout_marginTop="39dp"
android:layout_marginEnd="182dp"
android:progressTint="@color/white"
android:visibility="visible" />
<TextView
android:id="@+id/connectStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textAlignment="center" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<EditText
android:id="@+id/input"
android:inputType="number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_marginBottom="16dp"
android:fontFamily="sans-serif"/>
</LinearLayout>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/FirstFragment">
<fragment
android:id="@+id/FirstFragment"
android:name="de.nicolasklier.livebeat.FirstFragment"
android:label="@string/first_fragment_label"
tools:layout="@layout/fragment_first" >
<action
android:id="@+id/action_FirstFragment_to_SecondFragment"
app:destination="@id/SecondFragment" />
</fragment>
<fragment
android:id="@+id/SecondFragment"
android:name="de.nicolasklier.livebeat.SecondFragment"
android:label="@string/second_fragment_label"
tools:layout="@layout/fragment_second" >
<action
android:id="@+id/action_SecondFragment_to_FirstFragment"
app:destination="@id/FirstFragment" />
</fragment>
</navigation>

View File

@@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">48dp</dimen>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">200dp</dimen>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">48dp</dimen>
</resources>

View File

@@ -9,4 +9,5 @@
<string name="hello_first_fragment">Hello first fragment</string>
<string name="hello_second_fragment">Hello second fragment. Arg: %1$s</string>
<string name="title_activity_setup">Setup</string>
</resources>

View File

@@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Livebeat" parent="Theme.AppCompat.Light">
<style name="Theme.Livebeat" parent="Theme.MaterialComponents.DayNight">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/black</item>
<item name="colorPrimaryVariant">@color/black2</item>

View File

@@ -7,7 +7,7 @@ buildscript {
}
dependencies {
classpath "com.android.tools.build:gradle:4.1.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

5
android_new/.idea/.gitignore generated vendored
View File

@@ -1,5 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
<excludeFolder url="file://$MODULE_DIR$/android" />
<excludeFolder url="file://$MODULE_DIR$/ios" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="FLOW" />
</component>
</project>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/android_new.iml" filepath="$PROJECT_DIR$/.idea/android_new.iml" />
</modules>
</component>
</project>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
<mapping directory="$PROJECT_DIR$/Livebeat" vcs="Git" />
</component>
</project>

View File

@@ -1,63 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="19050b14-6aa3-4201-be2a-b6c9fb417ff6" name="Default Changelist" comment="">
<change beforePath="$PROJECT_DIR$/../android/app/src/main/java/de/nicolasklier/livebeat/MainActivity.kt" beforeDir="false" afterPath="$PROJECT_DIR$/../android/app/src/main/java/de/nicolasklier/livebeat/MainActivity.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app.ts" beforeDir="false" afterPath="$PROJECT_DIR$/app.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config.ts" beforeDir="false" afterPath="$PROJECT_DIR$/config.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/endpoints/beat.ts" beforeDir="false" afterPath="$PROJECT_DIR$/endpoints/beat.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/endpoints/phone.ts" beforeDir="false" afterPath="$PROJECT_DIR$/endpoints/phone.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/lib/rabbit.ts" beforeDir="false" afterPath="$PROJECT_DIR$/lib/rabbit.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../frontend/src/app/api.service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/../frontend/src/app/api.service.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../frontend/src/app/map.worker.ts" beforeDir="false" afterPath="$PROJECT_DIR$/../frontend/src/app/map.worker.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../frontend/src/app/map/map.component.html" beforeDir="false" afterPath="$PROJECT_DIR$/../frontend/src/app/map/map.component.html" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/../package-lock.json" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />
</component>
<component name="ProjectId" id="1qRNQeCqRVVNuS6QKJoYRrUYOnM" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
<property name="WebServerToolWindowFactoryState" value="false" />
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="19050b14-6aa3-4201-be2a-b6c9fb417ff6" name="Default Changelist" comment="" />
<created>1617038467045</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1617038467045</updated>
<workItem from="1617038468601" duration="1438000" />
<workItem from="1617121449363" duration="83000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
<option name="oldMeFiltersMigrated" value="true" />
</component>
</project>

View File

@@ -7,14 +7,15 @@ import * as figlet from 'figlet';
import * as mongoose from 'mongoose';
import { exit } from 'process';
import * as winston from 'winston';
import { createServer } from 'http';
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 } from './endpoints/user';
import { hashPassword, randomPepper, randomString } from './lib/crypto';
import { RabbitMQ } from './lib/rabbit';
import { SocketManager } from './lib/socketio';
import { UserType } from './models/user/user.interface';
import { User } from './models/user/user.model';
@@ -27,7 +28,6 @@ export const JWT_SECRET = process.env.JWT_SECRET || "";
export const IS_DEBUG = process.env.DEBUG == 'true';
export let logger: winston.Logger;
export let rabbitmq: RabbitMQ;
async function run() {
const { combine, timestamp, label, printf, prettyPrint } = winston.format;
@@ -108,10 +108,8 @@ async function run() {
await User.create({
name: 'admin',
password: await hashPassword(randomPassword + salt + randomPepper()),
brokerToken: randomString(16),
salt,
createdAt: Date.now(),
lastLogin: 0,
lastLogin: new Date(0),
type: UserType.ADMIN
});
logger.info("===================================================");
@@ -124,14 +122,23 @@ async function run() {
/**
* HTTP server
*/
logger.debug("Preparing HTTP server ...")
const app = express();
app.use(express.json());
const server = createServer(app);
app.use(cors());
app.options('*', cors());
app.use(express.json());
app.use(bodyParser.json({ limit: '5kb' }));
app.use((req, res, next) => {
res.on('finish', () => {
const done = Date.now();
// Censor any user passwords
if (req.body.password != null) {
req.body.password = "***********";
}
logger.debug(`${req.method} - ${req.url} ${JSON.stringify(req.body)} -> ${res.statusCode}`);
});
next();
@@ -141,10 +148,6 @@ async function run() {
// User authentication
app.post('/user/login', (req, res) => LoginUser(req, res));
app.get('/user/rabbitlogin', (req, res) => LoginRabbitUser(req, res));
app.get('/user/vhost', (req, res) => VHost(req, res));
app.get('/user/resource', (req, res) => Resource(req, res));
app.get('/user/topic', (req, res) => Topic(req, res));
// CRUD user
app.get('/user/notification', MW_User, (req, res) => getNotification(req, res)); // Notifications
@@ -163,16 +166,11 @@ async function run() {
app.get('/beat/', MW_User, (req, res) => GetBeat(req, res));
app.get('/beat/stats', MW_User, (req, res) => GetBeatStats(req, res));
app.listen(config.http.port, config.http.host, () => {
const socketManager = new SocketManager(server);
server.listen(config.http.port, config.http.host, () => {
logger.info(`HTTP server is running at ${config.http.host}:${config.http.port}`);
});
/**
* Message broker
*/
rabbitmq = new RabbitMQ();
await rabbitmq.init();
logger.info("Connected with message broker.");
}
run();

View File

@@ -5,12 +5,16 @@ import { Beat } from "../models/beat/beat.model.";
import { Phone } from "../models/phone/phone.model";
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 }).exec();
const perPhone: any = {};
let totalBeats = 0;
if (phones[0] == undefined) return;
const phone = phones[0];
for (let i = 0; i < phones.length; i++) {
const beatCount = await Beat.countDocuments({ phone: phones[i] });
const beatCount = await Beat.countDocuments({ [phone.id]: phone.id });
perPhone[phones[i]._id] = {};
perPhone[phones[i]._id] = beatCount;
totalBeats += beatCount;

View File

@@ -1,5 +1,5 @@
import { Response } from "express";
import { logger, rabbitmq } from "../app";
import { logger } from "../app";
import { LivebeatRequest } from "../lib/request";
import { Beat } from "../models/beat/beat.model.";
import { Phone } from "../models/phone/phone.model";
@@ -66,7 +66,7 @@ export async function PostPhone(req: LivebeatRequest, res: Response) {
});
logger.info(`New device (${displayName}) registered for ${req.user?.name}.`);
rabbitmq.publish(req.user?.id, newPhone.toJSON(), 'phone_register')
//rabbitmq.publish(req.user?.id, newPhone.toJSON(), 'phone_register')
res.status(200).send();
}

View File

@@ -111,7 +111,7 @@ export async function LoginUser(req: Request, res: Response) {
}
// We're good. Create JWT token.
const token = sign({ user: user._id }, JWT_SECRET, { expiresIn: '30d' });
const token = sign({ user: user._id, type: 'frontend' }, JWT_SECRET, { expiresIn: '30d' });
user.lastLogin = new Date(Date.now());
await user.save();
@@ -120,159 +120,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.

View File

@@ -1,6 +1,7 @@
import { hash, verify } from 'argon2';
import { verify as jwtVerify } from 'jsonwebtoken';
import { config } from '../config';
import { IS_DEBUG, logger } from '../app';
import { IS_DEBUG, JWT_SECRET, logger } from '../app';
export async function hashPassword(input: string): Promise<string> {
const start = Date.now();
@@ -64,9 +65,19 @@ export async function verifyPassword(password: string, hashInput: string): Promi
});
}
export function randomString(length: number): string {
export async function verifyJWT(token: string): Promise<boolean> {
return new Promise<boolean>(async (resolve, reject) => {
try {
jwtVerify(token, JWT_SECRET, { algorithms: ['HS256'] });
resolve(true);
} catch {
resolve(false);
}
});
}
export function randomString(length: number, characters: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'): string {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const charactersLength = characters.length;
for ( let i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));

View File

@@ -1,23 +1,165 @@
import * as socketio from "socket.io";
import { Server } from 'http';
import { JWT_SECRET, logger } from "../app";
import { randomString, verifyJWT } from "./crypto";
import { decode, sign } from "jsonwebtoken";
import { User } from "../models/user/user.model";
import { IPhone } from "../models/phone/phone.interface";
import { IUser } from "../models/user/user.interface";
import { Phone } from "../models/phone/phone.model";
/**
* This class handles all SocketIO connections.
*
* *SocketIO is another layer ontop of WebSockets*
*/
class SocketManager {
export class SocketManager {
io: socketio.Server;
express: Express.Application;
constructor (express: Express.Application) {
this.io = new socketio.Server(express);
this.express = express;
/**
* Frontends have limited access to socket.io features. They just sit in connection and wait for any events.
*/
frontends: Array<string>;
/**
* A phone has some more privileges. They activly send new data and thus have write access.
*/
phones: Array<string>;
constructor(httpServer: Server) {
logger.debug("Preparing real-time communication ...");
this.frontends = [];
this.phones = [];
this.io = new socketio.Server();
this.io.listen(httpServer);
this.init();
}
getUserRoom(user: IUser) {
return `user-${user.id}`;
}
getUserFrontendRoom(user: IUser) {
return `user-${user.id}-frontend`;
}
getUserPhoneRoom(user: IUser) {
return `user-${user.id}-phone`;
}
init() {
this.io.on('connect', data => {
console.log('New connection')
this.io.on('connection', socket => {
socket.on('requestAccess', async data => {
data = JSON.parse(data);
let token: string = data.token;
let phone: IPhone = data.phone;
// If request is faulty or token invalid -> return.
if (data === undefined || phone === undefined) return;
if (await !verifyJWT(token)) return;
const id = decode(token, { json: true })!.user;
const user = await User.findById(id);
// If user doesn't exist -> return.
if (user === null) return;
const approvalCode = randomString(6, '0123456789');
// Create phone
const newPhone = await Phone.create({
...phone,
user,
approval: {
code: approvalCode
}
});
this.io.to(this.getUserRoom(user)).emit('approvePhone', newPhone);
// Respond with id so device can later submit correct code.
socket.emit('requestAccess', { phoneId: newPhone.id });
logger.info(`User ${user?.name} requests to connect new phone ${phone.displayName}`);
});
socket.on('submitPairCode', async data => {
const { phoneId, code } = JSON.parse(data);
console.log("Entry:", data, phoneId, code);
if (phoneId === undefined || code === undefined) return;
const phone = await Phone.findById(phoneId);
if (phone === null) return;
console.log(data, phoneId, code);
// If provided code isn't equal with actual code -> Emit event again.
if (phone.approval.code !== code) {
console.log(data, phoneId, code);
socket.emit('submitPairCode', '');
return;
}
phone.approval.approvedOn = new Date();
await phone.save();
// We're good. Create JWT token.
const token = sign({ user: phone.user._id, type: 'phone' }, JWT_SECRET, { expiresIn: '30d' });
socket.emit('submitPairCode', token);
});
socket.on('loginFrontend', async (token: string) => {
if (await verifyJWT(token)) {
const tokenDecoded = decode(token, { json: true });
const id = tokenDecoded!.user;
const type = tokenDecoded!.type;
const user = await User.findById(id);
if (user == null) return;
if (type != 'frontend') return;
if (this.frontends.indexOf(socket.id) != -1)
this.frontends.push(socket.id);
socket.join(this.getUserRoom(user));
socket.join(this.getUserFrontendRoom(user));
logger.info(`Socket ${socket.id} became a frontend socket.`);
}
});
socket.on('loginPhone', async (token: string) => {
if (await verifyJWT(token)) {
const tokenDecoded = decode(token, { json: true });
const id = tokenDecoded!.user;
const type = tokenDecoded!.type;
const user = await User.findById(id);
if (user == null) return;
if (type != 'phone') return;
if (this.frontends.indexOf(socket.id) != -1)
this.frontends.push(socket.id);
socket.join(this.getUserRoom(user));
socket.join(this.getUserPhoneRoom(user));
logger.info(`Socket ${socket.id} became a phone socket.`);
}
});
logger.info(`New socket connection from ${socket.handshake.address} with id ${socket.id} (total connections: ${this.io.sockets.sockets.size})`);
socket.emit('test', 'Yay, it works.');
});
}

View File

@@ -4,11 +4,14 @@ import { IUser } from '../user/user.interface';
export interface IPhone extends Document {
androidId: String,
displayName: String,
modelName: String,
modelName: string,
operatingSystem: String,
architecture: String,
user: IUser,
active: Boolean,
approval: {
approvedOn?: Date,
code: String
},
updatedAt?: Date,
createdAt?: Date
}

View File

@@ -8,7 +8,10 @@ const schemaPhone = new Schema({
operatingSystem: { type: String, required: false },
architecture: { type: String, required: false },
user: { type: SchemaTypes.ObjectId, required: true },
active: { type: Boolean, required: true }
approval: {
approvedOn: { type: Date, required: false },
code: { type: String, required: true }
}
}, {
timestamps: {
createdAt: true,

View File

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

View File

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

3806
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,10 +17,7 @@
"author": "Mondei1",
"license": "GPL-3.0-or-later",
"dependencies": {
"@types/node": "^14.14.9",
"@types/socket.io": "^2.1.13",
"amqplib": "^0.6.0",
"argon2": "^0.27.0",
"argon2": "^0.28.2",
"body-parser": "^1.19.0",
"chalk": "^4.1.0",
"cors": "^2.8.5",
@@ -29,14 +26,13 @@
"figlet": "^1.5.0",
"jsonwebtoken": "^8.5.1",
"moment": "^2.29.1",
"mongoose": "^5.10.9",
"socket.io": "^4.0.1",
"mongoose": "^5.13.7",
"socket.io": "^4.3.2",
"ts-node": "^9.0.0",
"typescript": "^4.0.3",
"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",
@@ -47,6 +43,8 @@
"@types/jsonwebtoken": "8.5.0",
"@types/moment": "2.13.0",
"@types/mongoose": "5.7.36",
"@types/node": "^14.14.9",
"@types/socket.io": "^2.1.13",
"@types/typescript": "2.0.0",
"@types/winston": "2.4.4",
"concurrently": "^5.3.0",

3
backend_python/.idea/.gitignore generated vendored
View File

@@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@@ -1,8 +0,0 @@
<?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>

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

View File

@@ -1,7 +0,0 @@
<?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" />
<component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" />
</component>
</project>

View File

@@ -1,8 +0,0 @@
<?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>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View File

@@ -1,17 +0,0 @@
from flask import Flask, jsonify
from flask_restful import Api, Resource, reqparse, abort
import pymongo, pika
import vars
import resources.user as user
app = Flask(__name__)
api = Api(app)
api.add_resource(user.UserLogin, "/user/login")
if __name__ == '__main__':
vars.db = pymongo.MongoClient("mongodb+srv://backend:Rjmzs75W9EYwW8G7@cluster0.qxerq.mongodb.net/livebeat?retryWrites=true&w=majority")
print(vars.db.list_databases())
rabbit = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = rabbit.channel()
app.run(debug=True)

View File

@@ -1,7 +0,0 @@
from flask_restful import Api, Resource, reqparse, abort
import vars
class UserLogin(Resource):
def get(self):
print(vars.db['livebeat'])
return

View File

@@ -1,6 +0,0 @@
"""
This file contains all variables that are shared across the entire application.
For example: Database connection, sockets, etc.
"""
db = None

23785
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,16 +11,16 @@
},
"private": true,
"dependencies": {
"@angular/animations": "~10.1.5",
"@angular/animations": "~11.2.13",
"@angular/cdk": "^10.2.1",
"@angular/common": "~10.1.5",
"@angular/compiler": "~10.1.5",
"@angular/core": "~10.1.5",
"@angular/forms": "~10.1.5",
"@angular/platform-browser": "~10.1.5",
"@angular/platform-browser-dynamic": "~10.1.5",
"@angular/router": "~10.1.5",
"@angular/service-worker": "~10.1.5",
"@angular/common": "~11.2.13",
"@angular/compiler": "~11.2.13",
"@angular/core": "~11.2.13",
"@angular/forms": "~11.2.13",
"@angular/platform-browser": "~11.2.13",
"@angular/platform-browser-dynamic": "~11.2.13",
"@angular/router": "~11.2.13",
"@angular/service-worker": "~11.2.13",
"@fortawesome/angular-fontawesome": "^0.7.0",
"@fortawesome/fontawesome-svg-core": "^1.2.28",
"@fortawesome/free-brands-svg-icons": "^5.13.0",
@@ -31,19 +31,21 @@
"@types/moment": "^2.13.0",
"chart.js": "^2.9.4",
"eva-icons": "^1.1.3",
"font-awesome": "^4.7.0",
"geojson": "^0.5.0",
"mapbox-gl": "^1.12.0",
"moment": "^2.29.1",
"ng2-charts": "^2.4.2",
"ngx-mapbox-gl": "^4.8.1",
"ngx-socket-io": "^4.1.0",
"rxjs": "~6.6.0",
"tslib": "^2.0.0",
"zone.js": "~0.10.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.1001.6",
"@angular/cli": "~10.1.6",
"@angular/compiler-cli": "~10.1.5",
"@angular-devkit/build-angular": "~0.1102.12",
"@angular/cli": "~11.2.12",
"@angular/compiler-cli": "~11.2.13",
"@schematics/angular": "~10.1.6",
"@types/jasmine": "~3.5.0",
"@types/jasminewd2": "~2.0.3",
@@ -51,7 +53,7 @@
"codelyzer": "^6.0.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.0.0",
"karma": "~6.3.2",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0",

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { IPhone } from '../api.service';
export class Alert {
id: string;

View File

@@ -57,7 +57,13 @@
.alert-warning {
background-color: #F3CC17 !important;
color: #000 !important;
color: rgba(53, 92, 74, 0.8) !important;
}
.alert-pair {
height: 5rem !important;
background-color: rgba(62, 58, 143, 0.8) !important;
backdrop-filter: blur(20px);
}
.fadeOut {

View File

@@ -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';

View File

@@ -2,8 +2,8 @@ import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular
import { Injectable } from '@angular/core';
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';
import { Socket } from 'ngx-socket-io';
/*
* DEFINITION OF TYPE
@@ -57,6 +57,10 @@ export interface IPhone {
modelName: string;
operatingSystem: string;
architecture: string;
approval: {
approvedOn?: Date,
code: String
},
user: IUser;
updatedAt?: Date;
createdAt?: Date;
@@ -125,12 +129,27 @@ export class APIService {
loginEvent: BehaviorSubject<boolean> = new BehaviorSubject(false);
fetchingDataEvent: BehaviorSubject<boolean> = new BehaviorSubject(false);
API_ENDPOINT = 'http://192.168.178.26:8040';
API_ENDPOINT = 'http://localhost:8040';
constructor(private httpClient: HttpClient, private alert: AlertService) { }
constructor(private httpClient: HttpClient, private alert: AlertService, private socket: Socket) { }
private socketInit(): void {
// Connect with Socket:io after we received our user information
// Connect with Socket.io after we received our user information
this.socket.connect();
this.socket.on('connect', () => {
console.log("HERE: " + this.token);
this.socket.emit('loginFrontend', this.token);
});
this.socket.on('test', data => {
this.alert.info(data);
console.log('Received test:', data);
});
this.socket.on('approvePhone', (phone: IPhone) => {
this.alert.dynamic(`To pair ${phone.displayName}, type in code: ${phone.approval.code}`, AlertType.INFO, 'Pair', { duration: 0 });
});
/*this.mqtt.connect({
hostname: '192.168.178.26',

View File

@@ -8,10 +8,10 @@
<li class="navbar-right"><a [routerLink]="['/notifications']">
<img src="assets/message.svg">
</a></li>
<li class="navbar-right"><a [routerLink]="['/user', this.api.user._id]"
routerLinkActive="router-link-active">
<li class="navbar-right"><a [routerLink]="['/user', this.api.user._id]" routerLinkActive="router-link-active">
<fa-icon [icon]="faUser"></fa-icon>
{{this.api.username}}</a></li>
{{this.api.username}}
</a></li>
<li class="navbar-right"><a [routerLink]="['/admin']" routerLinkActive="router-link-active"
*ngIf="this.api.user.type == 'admin'">

View File

@@ -12,15 +12,17 @@
#header {
position: fixed;
top: 0;
left: 0;
width: 100vw;
top: 16px;
left: 50%;
transform: translateX(-50%);
width: 98vw;
height: fit-content;
padding-top: 0.8rem;
padding-bottom: 0.8rem;
background-color: #1d1d1dd9;
backdrop-filter: blur(30px);
box-shadow: 10px 10px 50px 0px rgba(0, 0, 0, 0.85);
border-radius: 8px;
& ul {
display: inline;
@@ -84,5 +86,5 @@
}
.header-spacer {
height: 3rem;
height: 4rem;
}

View File

@@ -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;
}
}

View File

@@ -20,8 +20,12 @@ 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 { SocketIoConfig, SocketIoModule } from 'ngx-socket-io';
const config: SocketIoConfig = { url: 'http://localhost:8988', options: {} };
const config: SocketIoConfig = { url: 'http://localhost:8040', options: {
transports: ['websocket'],
autoConnect: false
}};
@NgModule({
declarations: [
@@ -48,7 +52,8 @@ const config: SocketIoConfig = { url: 'http://localhost:8988', options: {} };
}),
ChartsModule,
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
FontAwesomeModule
FontAwesomeModule,
SocketIoModule.forRoot(config)
],
providers: [],
bootstrap: [AppComponent]

View File

@@ -8,7 +8,6 @@
.dwidget {
line-height: 0.5rem;
background-color: rgba(0, 0, 0, 0.25);
backdrop-filter: blur(30px);
width: fit-content;
padding: 1.5rem;
padding-right: 6rem !important;
@@ -25,8 +24,9 @@
.bgColor {
z-index: -1 !important;
position: absolute;
width: 8rem;
width: 6rem;
height: 1rem;
margin-left: 5px;
margin-left: 20px;
filter: blur(20px);
background-color: transparent !important;
}

View File

@@ -25,7 +25,7 @@ addEventListener('message', ({ data }) => {
progress++;
// Report progress every fifth loop
if (Math.trunc(progress / data.length * 100) % 3 === 0) {
if (Math.trunc(progress / data.length * 100) % 5 === 0) {
postMessage({ progress: (progress / data.length) * 100 });
}
@@ -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) {

View File

@@ -6,9 +6,15 @@
<h2 *ngIf="showDevices">Devices</h2>
<ul class="phoneListing" *ngIf="showDevices">
<li *ngFor="let phone of this.api.phones">
<h2 [ngClass]="{offline: !phone.active}">{{phone.displayName}} <span class="lastBeat">last beat was {{ this.lastBeats.get(phone._id) }}</span></h2>
<p>{{phone.modelName}}</p>
<li class="singlePhone" *ngFor="let phone of this.api.phones">
<img src="assets/phone.svg">
<h2 [ngClass]="{offline: !phone.active}">{{ phone.displayName }} <span class="smaller">last beat was {{ this.lastBeats.get(phone._id) }}</span></h2>
<p>{{phone.modelName}} <span class="smaller">| created at: {{ phone.createdAt }}</span></p>
<div *ngIf="phone.approval != null && phone.approval.approvedOn == null">
<small>Code</small>
<h3>{{ phone.approval.code }}</h3>
</div>
</li>
</ul>
</div>

View File

@@ -1,4 +1,4 @@
@import '../../styles.scss';
@import "../../styles.scss";
#user {
min-width: 40rem;
@@ -20,16 +20,53 @@
.phoneListing {
list-style: none;
padding: 1.5rem;
padding: 1rem;
border-radius: 10px;
background-color: $darker;
}
.offline {
color: #ff6464
.singlePhone {
display: grid;
grid-template-columns: 64px 1fr 5rem;
grid-template-rows: 1fr 1fr;
column-gap: 10rem;
gap: 0px 0px;
width: 100%;
height: 100%;
& img {
transform: translateY(50%);
width: 100%;
grid-area: 1 / 1 / 3 / 2;
}
& h2 {
margin-left: 1rem;
grid-area: 1 / 2 / 2 / 3;
}
& p {
margin-left: 1rem;
grid-area: 2 / 2 / 3 / 3;
}
/* Dirty but that's the approval code. */
& div {
small {
grid-area: 1 / 3 / 2 / 4;
}
h3 {
grid-area: 2 / 3 / 3 / 4;
}
}
}
.lastBeat {
.offline {
color: #ff6464;
}
.smaller {
font-weight: lighter;
font-size: 10pt;
}

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-device-mobile" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.25" stroke="#fff" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<rect x="7" y="4" width="10" height="16" rx="1"></rect>
<line x1="11" y1="5" x2="13" y2="5"></line>
<line x1="12" y1="17" x2="12" y2="17.01"></line>
</svg>

After

Width:  |  Height:  |  Size: 453 B

1
node_modules Symbolic link
View File

@@ -0,0 +1 @@
frontend/node_modules

394
package-lock.json generated
View File

@@ -1,394 +0,0 @@
{
"name": "Livebeat",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"dependencies": {
"socket.io": "^4.0.1"
}
},
"node_modules/@types/component-emitter": {
"version": "1.2.10",
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz",
"integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg=="
},
"node_modules/@types/cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg=="
},
"node_modules/@types/cors": {
"version": "2.8.10",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz",
"integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ=="
},
"node_modules/@types/node": {
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz",
"integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA=="
},
"node_modules/accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"dependencies": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/base64-arraybuffer": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
"integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"engines": {
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"node_modules/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-5.0.0.tgz",
"integrity": "sha512-BATIdDV3H1SrE9/u2BAotvsmjJg0t1P4+vGedImSs1lkFAtQdvk4Ev1y4LDiPF7BPWgXWEG+NDY+nLvW3UrMWw==",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~4.0.0",
"ws": "~7.4.2"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/engine.io-parser": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz",
"integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==",
"dependencies": {
"base64-arraybuffer": "0.1.4"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/mime-db": {
"version": "1.47.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz",
"integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.30",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz",
"integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==",
"dependencies": {
"mime-db": "1.47.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/socket.io": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.0.1.tgz",
"integrity": "sha512-g8eZB9lV0f4X4gndG0k7YZAywOg1VxYgCUspS4V+sDqsgI/duqd0AW84pKkbGj/wQwxrqrEq+VZrspRfTbHTAQ==",
"dependencies": {
"@types/cookie": "^0.4.0",
"@types/cors": "^2.8.8",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"debug": "~4.3.1",
"engine.io": "~5.0.0",
"socket.io-adapter": "~2.2.0",
"socket.io-parser": "~4.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-adapter": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.2.0.tgz",
"integrity": "sha512-rG49L+FwaVEwuAdeBRq49M97YI3ElVabJPzvHT9S6a2CWhDKnjSFasvwAwSYPRhQzfn4NtDIbCaGYgOCOU/rlg=="
},
"node_modules/socket.io-parser": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz",
"integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==",
"dependencies": {
"@types/component-emitter": "^1.2.10",
"component-emitter": "~1.3.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/ws": {
"version": "7.4.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz",
"integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
},
"dependencies": {
"@types/component-emitter": {
"version": "1.2.10",
"resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz",
"integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg=="
},
"@types/cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg=="
},
"@types/cors": {
"version": "2.8.10",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz",
"integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ=="
},
"@types/node": {
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz",
"integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA=="
},
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
"requires": {
"mime-types": "~2.1.24",
"negotiator": "0.6.2"
}
},
"base64-arraybuffer": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
"integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI="
},
"base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
},
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
},
"cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"requires": {
"object-assign": "^4",
"vary": "^1"
}
},
"debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"requires": {
"ms": "2.1.2"
}
},
"engine.io": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-5.0.0.tgz",
"integrity": "sha512-BATIdDV3H1SrE9/u2BAotvsmjJg0t1P4+vGedImSs1lkFAtQdvk4Ev1y4LDiPF7BPWgXWEG+NDY+nLvW3UrMWw==",
"requires": {
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~4.0.0",
"ws": "~7.4.2"
}
},
"engine.io-parser": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz",
"integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==",
"requires": {
"base64-arraybuffer": "0.1.4"
}
},
"mime-db": {
"version": "1.47.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz",
"integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw=="
},
"mime-types": {
"version": "2.1.30",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz",
"integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==",
"requires": {
"mime-db": "1.47.0"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"negotiator": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"socket.io": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.0.1.tgz",
"integrity": "sha512-g8eZB9lV0f4X4gndG0k7YZAywOg1VxYgCUspS4V+sDqsgI/duqd0AW84pKkbGj/wQwxrqrEq+VZrspRfTbHTAQ==",
"requires": {
"@types/cookie": "^0.4.0",
"@types/cors": "^2.8.8",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"debug": "~4.3.1",
"engine.io": "~5.0.0",
"socket.io-adapter": "~2.2.0",
"socket.io-parser": "~4.0.3"
}
},
"socket.io-adapter": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.2.0.tgz",
"integrity": "sha512-rG49L+FwaVEwuAdeBRq49M97YI3ElVabJPzvHT9S6a2CWhDKnjSFasvwAwSYPRhQzfn4NtDIbCaGYgOCOU/rlg=="
},
"socket.io-parser": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz",
"integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==",
"requires": {
"@types/component-emitter": "^1.2.10",
"component-emitter": "~1.3.0",
"debug": "~4.3.1"
}
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"ws": {
"version": "7.4.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz",
"integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==",
"requires": {}
}
}
}

View File

@@ -1,5 +0,0 @@
{
"dependencies": {
"socket.io": "^4.0.1"
}
}