Compare commits
3 Commits
feature/re
...
19b7c05d75
| Author | SHA1 | Date | |
|---|---|---|---|
|
19b7c05d75
|
|||
|
28e85ea730
|
|||
|
9f1aa50681
|
122
android/.idea/codeStyles/Project.xml
generated
Normal file
122
android/.idea/codeStyles/Project.xml
generated
Normal 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>
|
||||||
5
android/.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
android/.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
2
android/.idea/compiler.xml
generated
2
android/.idea/compiler.xml
generated
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="CompilerConfiguration">
|
<component name="CompilerConfiguration">
|
||||||
<bytecodeTargetLevel target="1.8" />
|
<bytecodeTargetLevel target="11" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
3
android/.idea/gradle.xml
generated
3
android/.idea/gradle.xml
generated
@@ -4,7 +4,7 @@
|
|||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="testRunner" value="PLATFORM" />
|
<option name="testRunner" value="GRADLE" />
|
||||||
<option name="disableWrapperSourceDistributionNotification" value="true" />
|
<option name="disableWrapperSourceDistributionNotification" value="true" />
|
||||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
<option name="resolveModulePerSourceSet" value="false" />
|
<option name="resolveModulePerSourceSet" value="false" />
|
||||||
<option name="useQualifiedModuleNames" value="true" />
|
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
13
android/.idea/misc.xml
generated
13
android/.idea/misc.xml
generated
@@ -1,6 +1,17 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<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" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectType">
|
<component name="ProjectType">
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ android {
|
|||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
debuggable false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@@ -30,10 +31,17 @@ android {
|
|||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = '1.8'
|
jvmTarget = '1.8'
|
||||||
}
|
}
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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 "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
implementation 'androidx.core:core-ktx:1.3.2'
|
implementation 'androidx.core:core-ktx:1.3.2'
|
||||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||||
@@ -46,6 +54,6 @@ dependencies {
|
|||||||
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"
|
||||||
testImplementation 'junit:junit:4.+'
|
testImplementation 'junit:junit:4.+'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||||
}
|
}
|
||||||
BIN
android/app/libs/lineage-sdk.jar
Normal file
BIN
android/app/libs/lineage-sdk.jar
Normal file
Binary file not shown.
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
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.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
@@ -19,18 +19,26 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
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" />
|
<service android:name=".TrackerService" />
|
||||||
<receiver android:name=".BootReceiver">
|
|
||||||
<intent-filter >
|
<receiver android:name=".BootReceiver" >
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/Theme.Livebeat.NoActionBar">
|
android:theme="@style/Theme.Livebeat.NoActionBar" >
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
|||||||
@@ -10,16 +10,13 @@ import android.content.pm.PackageManager
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.telephony.TelephonyManager
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
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 androidx.annotation.ColorInt
|
||||||
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
|
||||||
@@ -27,11 +24,9 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
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.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.util.logging.Logger
|
|
||||||
|
|
||||||
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
|
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
@@ -46,6 +41,15 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
@SuppressLint("HardwareIds")
|
@SuppressLint("HardwareIds")
|
||||||
fun checkIfPhoneIsRegistered() {
|
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;
|
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)
|
||||||
@@ -95,8 +99,6 @@ 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")
|
|
||||||
|
|
||||||
// Check authorization
|
// Check authorization
|
||||||
val backendChecks = Thread(Runnable {
|
val backendChecks = Thread(Runnable {
|
||||||
val username = findViewById<TextView>(R.id.username).text
|
val username = findViewById<TextView>(R.id.username).text
|
||||||
@@ -142,7 +144,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
USER = jsonToUser.fromJson(userInfoResponseBody)
|
USER = jsonToUser.fromJson(userInfoResponseBody)
|
||||||
|
|
||||||
// Only start service if authentication went good.
|
// 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)
|
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Login succeeded", Snackbar.LENGTH_SHORT)
|
||||||
.setBackgroundTint(Color.GREEN)
|
.setBackgroundTint(Color.GREEN)
|
||||||
@@ -174,14 +176,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val statusRabbit = intent.getBooleanExtra("statusRabbit", false)
|
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)
|
||||||
@@ -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() {
|
private fun checkPerms() {
|
||||||
if (ActivityCompat.checkSelfPermission(
|
if (ActivityCompat.checkSelfPermission(
|
||||||
this,
|
this,
|
||||||
|
|||||||
@@ -22,12 +22,21 @@ class Phone(
|
|||||||
val architecture: String
|
val architecture: String
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
class PhoneRegistration (
|
||||||
|
val phone: Phone,
|
||||||
|
val token: String
|
||||||
|
) {}
|
||||||
|
|
||||||
|
class PhoneSubmitPairCode (
|
||||||
|
val phoneId: String,
|
||||||
|
val code: String
|
||||||
|
) {}
|
||||||
|
|
||||||
class User(
|
class User(
|
||||||
val name: String,
|
val name: String,
|
||||||
val type: String,
|
val type: String,
|
||||||
val lastLogin: String,
|
val lastLogin: String,
|
||||||
val twoFASecret: String?,
|
val twoFASecret: String?,
|
||||||
val brokerToken: String,
|
|
||||||
val createdAt: String
|
val createdAt: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,19 +16,27 @@ import android.provider.Settings
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.app.NotificationCompat
|
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.Channel
|
||||||
import com.rabbitmq.client.Connection
|
import com.rabbitmq.client.Connection
|
||||||
import com.rabbitmq.client.ConnectionFactory
|
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 io.socket.client.IO
|
||||||
|
import io.socket.client.Socket
|
||||||
|
import io.socket.emitter.Emitter
|
||||||
|
import okhttp3.EventListener
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.lang.Exception
|
||||||
import java.util.concurrent.TimeoutException
|
import java.util.concurrent.TimeoutException
|
||||||
|
|
||||||
class TrackerService : Service() {
|
class TrackerService : Service() {
|
||||||
|
|
||||||
|
var isSocketConnected = false;
|
||||||
val conn = arrayOfNulls<Connection>(1)
|
val conn = arrayOfNulls<Connection>(1)
|
||||||
val channel = arrayOfNulls<Channel>(1)
|
val channel = arrayOfNulls<Channel>(1)
|
||||||
|
|
||||||
@@ -36,35 +44,47 @@ class TrackerService : Service() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun connectWithBroker() {
|
private fun connectSocket() {
|
||||||
// This thread only connects to RabbitMQ
|
// This thread only connects to RabbitMQ
|
||||||
val connectionThread = Thread(Runnable {
|
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 {
|
try {
|
||||||
conn[0] = factory.newConnection()
|
socket = IO.socket("http://192.168.178.26:8040", options)
|
||||||
channel[0] = conn[0]?.createChannel()
|
socket.connect()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Socket.io", "Unable to connect to socket: ${e.message}")
|
||||||
|
return@Runnable
|
||||||
|
}
|
||||||
|
|
||||||
val intent = Intent("de.nicolasklier.livebeat")
|
socket.on("test") { args ->
|
||||||
val bundle = Bundle()
|
run {
|
||||||
bundle.putBoolean("statusRabbit", true)
|
Log.i("Socket.io", args[0].toString());
|
||||||
intent.putExtras(bundle)
|
}
|
||||||
this.sendBroadcast(intent)
|
}
|
||||||
|
|
||||||
channel[0]?.queueDeclare("tracker-" + factory.username, true, false, false, null)
|
socket.on("connect") { args ->
|
||||||
//channel[0]?.basicPublish("", "Tracker", null, "Test message".toByteArray())
|
run {
|
||||||
Log.i("RabbitMQ", "run: Published test message")
|
isSocketConnected = true;
|
||||||
} catch (e: IOException) {
|
}
|
||||||
e.printStackTrace()
|
}
|
||||||
} catch (e: TimeoutException) {
|
|
||||||
e.printStackTrace()
|
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()
|
connectionThread.start()
|
||||||
@@ -105,13 +125,15 @@ class TrackerService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connectWithBroker()
|
connectSocket()
|
||||||
startForeground()
|
start()
|
||||||
return super.onStartCommand(intent, flags, startId)
|
return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startForeground() {
|
private fun start() {
|
||||||
val noticicationIntent = Intent(this, MainActivity::class.java)
|
val noticicationIntent = Intent(this, MainActivity::class.java).apply {
|
||||||
|
action = "Stop"
|
||||||
|
}
|
||||||
val pendingIntent = PendingIntent.getActivity(this, 0, noticicationIntent, 0)
|
val pendingIntent = PendingIntent.getActivity(this, 0, noticicationIntent, 0)
|
||||||
val chan = NotificationChannel(
|
val chan = NotificationChannel(
|
||||||
NOTIF_CHANNEL_ID,
|
NOTIF_CHANNEL_ID,
|
||||||
@@ -121,17 +143,24 @@ class TrackerService : Service() {
|
|||||||
chan.lockscreenVisibility = Notification.VISIBILITY_SECRET
|
chan.lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||||
val manager = (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
|
val manager = (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
|
||||||
manager.createNotificationChannel(chan)
|
manager.createNotificationChannel(chan)
|
||||||
startForeground(NOTIF_ID, NotificationCompat.Builder(this, NOTIF_CHANNEL_ID)
|
|
||||||
.setOngoing(true)
|
val notification = NotificationCompat.Builder(this, NOTIF_CHANNEL_ID)
|
||||||
.setContentTitle("Livebeat")
|
.setOngoing(true)
|
||||||
.setContentText("Tracker is running")
|
.setContentTitle("Livebeat")
|
||||||
.setContentIntent(pendingIntent)
|
.setContentText("Tracker is running")
|
||||||
.setCategory(Notification.CATEGORY_SERVICE)
|
.setContentIntent(pendingIntent)
|
||||||
.setPriority(NotificationManager.IMPORTANCE_LOW)
|
.setCategory(Notification.CATEGORY_SERVICE)
|
||||||
.setChannelId(NOTIF_CHANNEL_ID)
|
.setPriority(NotificationManager.IMPORTANCE_LOW)
|
||||||
.setColorized(true)
|
.setSmallIcon(R.mipmap.ic_launcher)
|
||||||
.setColor(Color.BLACK)
|
.setChannelId(NOTIF_CHANNEL_ID)
|
||||||
.build())
|
.setColorized(true)
|
||||||
|
.setShowWhen(false)
|
||||||
|
.setVisibility(VISIBILITY_SECRET)
|
||||||
|
.setColor(Color.BLACK)
|
||||||
|
.addAction(R.drawable.ic_launcher_background, "Stop", pendingIntent)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
manager.notify(0, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -64,36 +64,6 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</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
|
<Space
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="120dp" />
|
android:layout_height="120dp" />
|
||||||
|
|||||||
119
android/app/src/main/res/layout/activity_setup.xml
Normal file
119
android/app/src/main/res/layout/activity_setup.xml
Normal 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>
|
||||||
16
android/app/src/main/res/layout/input_dialog.xml
Normal file
16
android/app/src/main/res/layout/input_dialog.xml
Normal 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>
|
||||||
28
android/app/src/main/res/navigation/nav_graph.xml
Normal file
28
android/app/src/main/res/navigation/nav_graph.xml
Normal 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>
|
||||||
3
android/app/src/main/res/values-land/dimens.xml
Normal file
3
android/app/src/main/res/values-land/dimens.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<dimen name="fab_margin">48dp</dimen>
|
||||||
|
</resources>
|
||||||
3
android/app/src/main/res/values-w1240dp/dimens.xml
Normal file
3
android/app/src/main/res/values-w1240dp/dimens.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<dimen name="fab_margin">200dp</dimen>
|
||||||
|
</resources>
|
||||||
3
android/app/src/main/res/values-w600dp/dimens.xml
Normal file
3
android/app/src/main/res/values-w600dp/dimens.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<dimen name="fab_margin">48dp</dimen>
|
||||||
|
</resources>
|
||||||
@@ -9,4 +9,5 @@
|
|||||||
|
|
||||||
<string name="hello_first_fragment">Hello first fragment</string>
|
<string name="hello_first_fragment">Hello first fragment</string>
|
||||||
<string name="hello_second_fragment">Hello second fragment. Arg: %1$s</string>
|
<string name="hello_second_fragment">Hello second fragment. Arg: %1$s</string>
|
||||||
|
<string name="title_activity_setup">Setup</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="Theme.Livebeat" parent="Theme.AppCompat.Light">
|
<style name="Theme.Livebeat" parent="Theme.MaterialComponents.DayNight">
|
||||||
<!-- Primary brand color. -->
|
<!-- Primary brand color. -->
|
||||||
<item name="colorPrimary">@color/black</item>
|
<item name="colorPrimary">@color/black</item>
|
||||||
<item name="colorPrimaryVariant">@color/black2</item>
|
<item name="colorPrimaryVariant">@color/black2</item>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ buildscript {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "com.android.tools.build:gradle:4.1.0"
|
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
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ 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 { createServer } from 'http';
|
||||||
|
|
||||||
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 } from './endpoints/user';
|
||||||
import { hashPassword, randomPepper, randomString } from './lib/crypto';
|
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 { UserType } from './models/user/user.interface';
|
||||||
import { User } from './models/user/user.model';
|
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 const IS_DEBUG = process.env.DEBUG == 'true';
|
||||||
|
|
||||||
export let logger: winston.Logger;
|
export let logger: winston.Logger;
|
||||||
export let rabbitmq: RabbitMQ;
|
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
const { combine, timestamp, label, printf, prettyPrint } = winston.format;
|
const { combine, timestamp, label, printf, prettyPrint } = winston.format;
|
||||||
@@ -108,10 +108,8 @@ 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),
|
|
||||||
salt,
|
salt,
|
||||||
createdAt: Date.now(),
|
lastLogin: new Date(0),
|
||||||
lastLogin: 0,
|
|
||||||
type: UserType.ADMIN
|
type: UserType.ADMIN
|
||||||
});
|
});
|
||||||
logger.info("===================================================");
|
logger.info("===================================================");
|
||||||
@@ -124,14 +122,23 @@ async function run() {
|
|||||||
/**
|
/**
|
||||||
* HTTP server
|
* HTTP server
|
||||||
*/
|
*/
|
||||||
|
logger.debug("Preparing HTTP server ...")
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
const server = createServer(app);
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
app.options('*', cors());
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
app.use(bodyParser.json({ limit: '5kb' }));
|
app.use(bodyParser.json({ limit: '5kb' }));
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
res.on('finish', () => {
|
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}`);
|
logger.debug(`${req.method} - ${req.url} ${JSON.stringify(req.body)} -> ${res.statusCode}`);
|
||||||
});
|
});
|
||||||
next();
|
next();
|
||||||
@@ -141,10 +148,6 @@ async function run() {
|
|||||||
|
|
||||||
// User authentication
|
// User authentication
|
||||||
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
|
||||||
@@ -163,16 +166,11 @@ async function run() {
|
|||||||
app.get('/beat/', MW_User, (req, res) => GetBeat(req, res));
|
app.get('/beat/', MW_User, (req, res) => GetBeat(req, res));
|
||||||
app.get('/beat/stats', MW_User, (req, res) => GetBeatStats(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}`);
|
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();
|
run();
|
||||||
@@ -5,12 +5,16 @@ import { Beat } from "../models/beat/beat.model.";
|
|||||||
import { Phone } from "../models/phone/phone.model";
|
import { Phone } from "../models/phone/phone.model";
|
||||||
|
|
||||||
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 }).exec();
|
||||||
const perPhone: any = {};
|
const perPhone: any = {};
|
||||||
let totalBeats = 0;
|
let totalBeats = 0;
|
||||||
|
|
||||||
|
if (phones[0] == undefined) return;
|
||||||
|
|
||||||
|
const phone = phones[0];
|
||||||
|
|
||||||
for (let i = 0; i < phones.length; i++) {
|
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] = {};
|
||||||
perPhone[phones[i]._id] = beatCount;
|
perPhone[phones[i]._id] = beatCount;
|
||||||
totalBeats += beatCount;
|
totalBeats += beatCount;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Response } from "express";
|
import { Response } from "express";
|
||||||
import { logger, rabbitmq } from "../app";
|
import { logger } from "../app";
|
||||||
import { LivebeatRequest } from "../lib/request";
|
import { LivebeatRequest } from "../lib/request";
|
||||||
import { Beat } from "../models/beat/beat.model.";
|
import { Beat } from "../models/beat/beat.model.";
|
||||||
import { Phone } from "../models/phone/phone.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}.`);
|
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();
|
res.status(200).send();
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@ export async function LoginUser(req: Request, res: Response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We're good. Create JWT token.
|
// 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());
|
user.lastLogin = new Date(Date.now());
|
||||||
await user.save();
|
await user.save();
|
||||||
@@ -120,159 +120,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.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { hash, verify } from 'argon2';
|
import { hash, verify } from 'argon2';
|
||||||
|
import { verify as jwtVerify } from 'jsonwebtoken';
|
||||||
import { config } from '../config';
|
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> {
|
export async function hashPassword(input: string): Promise<string> {
|
||||||
const start = Date.now();
|
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 = '';
|
let result = '';
|
||||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
||||||
const charactersLength = characters.length;
|
const charactersLength = characters.length;
|
||||||
for ( let i = 0; i < length; i++ ) {
|
for ( let i = 0; i < length; i++ ) {
|
||||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import * as amqp from 'amqplib';
|
|
||||||
import { Schema, SchemaType } from 'mongoose';
|
|
||||||
import { logger, RABBITMQ_URI } from '../app';
|
import { logger, RABBITMQ_URI } from '../app';
|
||||||
import { Beat } from '../models/beat/beat.model.';
|
import { Beat } from '../models/beat/beat.model.';
|
||||||
import { ISeverity, NotificationType } from '../models/notifications/notification.interface';
|
import { ISeverity, NotificationType } from '../models/notifications/notification.interface';
|
||||||
166
backend/lib/socketio.ts
Normal file
166
backend/lib/socketio.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
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*
|
||||||
|
*/
|
||||||
|
export class SocketManager {
|
||||||
|
|
||||||
|
io: socketio.Server;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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('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.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,11 +4,14 @@ import { IUser } from '../user/user.interface';
|
|||||||
export interface IPhone extends Document {
|
export interface IPhone extends Document {
|
||||||
androidId: String,
|
androidId: String,
|
||||||
displayName: String,
|
displayName: String,
|
||||||
modelName: String,
|
modelName: string,
|
||||||
operatingSystem: String,
|
operatingSystem: String,
|
||||||
architecture: String,
|
architecture: String,
|
||||||
user: IUser,
|
user: IUser,
|
||||||
active: Boolean,
|
approval: {
|
||||||
|
approvedOn?: Date,
|
||||||
|
code: String
|
||||||
|
},
|
||||||
updatedAt?: Date,
|
updatedAt?: Date,
|
||||||
createdAt?: Date
|
createdAt?: Date
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,10 @@ const schemaPhone = new Schema({
|
|||||||
operatingSystem: { type: String, required: false },
|
operatingSystem: { type: String, required: false },
|
||||||
architecture: { type: String, required: false },
|
architecture: { type: String, required: false },
|
||||||
user: { type: SchemaTypes.ObjectId, required: true },
|
user: { type: SchemaTypes.ObjectId, required: true },
|
||||||
active: { type: Boolean, required: true }
|
approval: {
|
||||||
|
approvedOn: { type: Date, required: false },
|
||||||
|
code: { type: String, required: true }
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
timestamps: {
|
timestamps: {
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
|||||||
@@ -12,7 +12,5 @@ export interface IUser extends Document {
|
|||||||
salt: string,
|
salt: string,
|
||||||
type: UserType,
|
type: UserType,
|
||||||
lastLogin: Date,
|
lastLogin: Date,
|
||||||
twoFASecret?: string,
|
twoFASecret?: string
|
||||||
brokerToken: string,
|
|
||||||
createdAt?: Date
|
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,6 @@ 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 },
|
|
||||||
lastLogin: { type: Date, required: true, default: Date.now },
|
lastLogin: { type: Date, required: true, default: Date.now },
|
||||||
}, {
|
}, {
|
||||||
timestamps: {
|
timestamps: {
|
||||||
|
|||||||
6117
backend/package-lock.json
generated
6117
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -17,9 +17,7 @@
|
|||||||
"author": "Mondei1",
|
"author": "Mondei1",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^14.14.9",
|
"argon2": "^0.28.2",
|
||||||
"amqplib": "^0.6.0",
|
|
||||||
"argon2": "^0.27.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",
|
||||||
@@ -28,7 +26,8 @@
|
|||||||
"figlet": "^1.5.0",
|
"figlet": "^1.5.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"mongoose": "^5.10.9",
|
"mongoose": "^5.13.7",
|
||||||
|
"socket.io": "^4.3.2",
|
||||||
"ts-node": "^9.0.0",
|
"ts-node": "^9.0.0",
|
||||||
"typescript": "^4.0.3",
|
"typescript": "^4.0.3",
|
||||||
"winston": "^3.3.3"
|
"winston": "^3.3.3"
|
||||||
@@ -37,17 +36,18 @@
|
|||||||
"@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/node": "^14.14.9",
|
||||||
|
"@types/socket.io": "^2.1.13",
|
||||||
"@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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
backend_python/.idea/.gitignore
generated
vendored
3
backend_python/.idea/.gitignore
generated
vendored
@@ -1,3 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
8
backend_python/.idea/backend.iml
generated
8
backend_python/.idea/backend.iml
generated
@@ -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>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<settings>
|
|
||||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
|
||||||
<version value="1.0" />
|
|
||||||
</settings>
|
|
||||||
</component>
|
|
||||||
7
backend_python/.idea/misc.xml
generated
7
backend_python/.idea/misc.xml
generated
@@ -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>
|
|
||||||
8
backend_python/.idea/modules.xml
generated
8
backend_python/.idea/modules.xml
generated
@@ -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>
|
|
||||||
6
backend_python/.idea/vcs.xml
generated
6
backend_python/.idea/vcs.xml
generated
@@ -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>
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -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)
|
|
||||||
Binary file not shown.
@@ -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
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
"""
|
|
||||||
This file contains all variables that are shared across the entire application.
|
|
||||||
For example: Database connection, sockets, etc.
|
|
||||||
"""
|
|
||||||
|
|
||||||
db = None
|
|
||||||
10346
frontend/package-lock.json
generated
10346
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,16 +11,16 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "~10.1.5",
|
"@angular/animations": "~11.2.13",
|
||||||
"@angular/cdk": "^10.2.1",
|
"@angular/cdk": "^10.2.1",
|
||||||
"@angular/common": "~10.1.5",
|
"@angular/common": "~11.2.13",
|
||||||
"@angular/compiler": "~10.1.5",
|
"@angular/compiler": "~11.2.13",
|
||||||
"@angular/core": "~10.1.5",
|
"@angular/core": "~11.2.13",
|
||||||
"@angular/forms": "~10.1.5",
|
"@angular/forms": "~11.2.13",
|
||||||
"@angular/platform-browser": "~10.1.5",
|
"@angular/platform-browser": "~11.2.13",
|
||||||
"@angular/platform-browser-dynamic": "~10.1.5",
|
"@angular/platform-browser-dynamic": "~11.2.13",
|
||||||
"@angular/router": "~10.1.5",
|
"@angular/router": "~11.2.13",
|
||||||
"@angular/service-worker": "~10.1.5",
|
"@angular/service-worker": "~11.2.13",
|
||||||
"@fortawesome/angular-fontawesome": "^0.7.0",
|
"@fortawesome/angular-fontawesome": "^0.7.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.28",
|
"@fortawesome/fontawesome-svg-core": "^1.2.28",
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.13.0",
|
"@fortawesome/free-brands-svg-icons": "^5.13.0",
|
||||||
@@ -31,20 +31,21 @@
|
|||||||
"@types/moment": "^2.13.0",
|
"@types/moment": "^2.13.0",
|
||||||
"chart.js": "^2.9.4",
|
"chart.js": "^2.9.4",
|
||||||
"eva-icons": "^1.1.3",
|
"eva-icons": "^1.1.3",
|
||||||
|
"font-awesome": "^4.7.0",
|
||||||
"geojson": "^0.5.0",
|
"geojson": "^0.5.0",
|
||||||
"mapbox-gl": "^1.12.0",
|
"mapbox-gl": "^1.12.0",
|
||||||
"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",
|
"ngx-socket-io": "^4.1.0",
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "~0.1001.6",
|
"@angular-devkit/build-angular": "~0.1102.12",
|
||||||
"@angular/cli": "~10.1.6",
|
"@angular/cli": "~11.2.12",
|
||||||
"@angular/compiler-cli": "~10.1.5",
|
"@angular/compiler-cli": "~11.2.13",
|
||||||
"@schematics/angular": "~10.1.6",
|
"@schematics/angular": "~10.1.6",
|
||||||
"@types/jasmine": "~3.5.0",
|
"@types/jasmine": "~3.5.0",
|
||||||
"@types/jasminewd2": "~2.0.3",
|
"@types/jasminewd2": "~2.0.3",
|
||||||
@@ -52,7 +53,7 @@
|
|||||||
"codelyzer": "^6.0.0",
|
"codelyzer": "^6.0.0",
|
||||||
"jasmine-core": "~3.6.0",
|
"jasmine-core": "~3.6.0",
|
||||||
"jasmine-spec-reporter": "~5.0.0",
|
"jasmine-spec-reporter": "~5.0.0",
|
||||||
"karma": "~5.0.0",
|
"karma": "~6.3.2",
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||||
"karma-jasmine": "~4.0.0",
|
"karma-jasmine": "~4.0.0",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable, Subject } from 'rxjs';
|
import { Observable, Subject } from 'rxjs';
|
||||||
|
import { IPhone } from '../api.service';
|
||||||
|
|
||||||
export class Alert {
|
export class Alert {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -57,7 +57,13 @@
|
|||||||
|
|
||||||
.alert-warning {
|
.alert-warning {
|
||||||
background-color: #F3CC17 !important;
|
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 {
|
.fadeOut {
|
||||||
|
|||||||
@@ -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,10 +1,9 @@
|
|||||||
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';
|
import { Socket } from 'ngx-socket-io';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* DEFINITION OF TYPE
|
* DEFINITION OF TYPE
|
||||||
@@ -58,6 +57,10 @@ export interface IPhone {
|
|||||||
modelName: string;
|
modelName: string;
|
||||||
operatingSystem: string;
|
operatingSystem: string;
|
||||||
architecture: string;
|
architecture: string;
|
||||||
|
approval: {
|
||||||
|
approvedOn?: Date,
|
||||||
|
code: String
|
||||||
|
},
|
||||||
user: IUser;
|
user: IUser;
|
||||||
updatedAt?: Date;
|
updatedAt?: Date;
|
||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
@@ -126,13 +129,29 @@ export class APIService {
|
|||||||
loginEvent: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
loginEvent: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||||
fetchingDataEvent: 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 mqtt: MqttService, private alert: AlertService) { }
|
constructor(private httpClient: HttpClient, private alert: AlertService, private socket: Socket) { }
|
||||||
|
|
||||||
private mqttInit(): void {
|
private socketInit(): void {
|
||||||
// Connect with RabbitMQ after we received our user information
|
// Connect with Socket.io after we received our user information
|
||||||
this.mqtt.connect({
|
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',
|
hostname: '192.168.178.26',
|
||||||
port: 15675,
|
port: 15675,
|
||||||
protocol: 'ws',
|
protocol: 'ws',
|
||||||
@@ -163,7 +182,7 @@ export class APIService {
|
|||||||
this.alert.dynamic('Device is now offline', obj.severity, obj.displayName);
|
this.alert.dynamic('Device is now offline', obj.severity, obj.displayName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -181,7 +200,7 @@ export class APIService {
|
|||||||
await this.getUserInfo();
|
await this.getUserInfo();
|
||||||
await this.getNotifications();
|
await this.getNotifications();
|
||||||
|
|
||||||
this.mqttInit();
|
this.socketInit();
|
||||||
|
|
||||||
await this.getBeats();
|
await this.getBeats();
|
||||||
await this.getBeatStats();
|
await this.getBeatStats();
|
||||||
@@ -216,9 +235,9 @@ export class APIService {
|
|||||||
|
|
||||||
resolve(res.setupToken);
|
resolve(res.setupToken);
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<li><a [routerLink]="['/map']" routerLinkActive="router-link-active">Map</a></li>
|
<li><a [routerLink]="['/map']" routerLinkActive="router-link-active">Map</a></li>
|
||||||
|
|
||||||
<li class="navbar-right"><a [routerLink]="['/notifications']">
|
<li class="navbar-right"><a [routerLink]="['/notifications']">
|
||||||
<img src="assets/message.svg">
|
<img src="assets/message.svg">
|
||||||
</a></li>
|
</a></li>
|
||||||
<li class="navbar-right"><a [routerLink]="['/user', this.api.user._id]"
|
<li class="navbar-right"><a [routerLink]="['/user', this.api.user._id]" routerLinkActive="router-link-active">
|
||||||
routerLinkActive="router-link-active">
|
|
||||||
<fa-icon [icon]="faUser"></fa-icon>
|
<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"
|
<li class="navbar-right"><a [routerLink]="['/admin']" routerLinkActive="router-link-active"
|
||||||
*ngIf="this.api.user.type == 'admin'">
|
*ngIf="this.api.user.type == 'admin'">
|
||||||
|
|||||||
@@ -12,15 +12,17 @@
|
|||||||
|
|
||||||
#header {
|
#header {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 16px;
|
||||||
left: 0;
|
left: 50%;
|
||||||
width: 100vw;
|
transform: translateX(-50%);
|
||||||
|
width: 98vw;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
padding-top: 0.8rem;
|
padding-top: 0.8rem;
|
||||||
padding-bottom: 0.8rem;
|
padding-bottom: 0.8rem;
|
||||||
background-color: #1d1d1dd9;
|
background-color: #1d1d1dd9;
|
||||||
backdrop-filter: blur(30px);
|
backdrop-filter: blur(30px);
|
||||||
box-shadow: 10px 10px 50px 0px rgba(0, 0, 0, 0.85);
|
box-shadow: 10px 10px 50px 0px rgba(0, 0, 0, 0.85);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
& ul {
|
& ul {
|
||||||
display: inline;
|
display: inline;
|
||||||
@@ -84,5 +86,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-spacer {
|
.header-spacer {
|
||||||
height: 3rem;
|
height: 4rem;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,18 @@ 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 { SocketIoConfig, SocketIoModule } from 'ngx-socket-io';
|
||||||
|
|
||||||
|
const config: SocketIoConfig = { url: 'http://localhost:8040', options: {
|
||||||
|
transports: ['websocket'],
|
||||||
|
autoConnect: false
|
||||||
|
}};
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -42,13 +47,13 @@ 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'
|
||||||
}),
|
}),
|
||||||
ChartsModule,
|
ChartsModule,
|
||||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
||||||
FontAwesomeModule
|
FontAwesomeModule,
|
||||||
|
SocketIoModule.forRoot(config)
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
.dwidget {
|
.dwidget {
|
||||||
line-height: 0.5rem;
|
line-height: 0.5rem;
|
||||||
background-color: rgba(0, 0, 0, 0.25);
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
backdrop-filter: blur(30px);
|
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
padding-right: 6rem !important;
|
padding-right: 6rem !important;
|
||||||
@@ -25,8 +24,9 @@
|
|||||||
.bgColor {
|
.bgColor {
|
||||||
z-index: -1 !important;
|
z-index: -1 !important;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 8rem;
|
width: 6rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
margin-left: 5px;
|
margin-left: 20px;
|
||||||
|
filter: blur(20px);
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ addEventListener('message', ({ data }) => {
|
|||||||
progress++;
|
progress++;
|
||||||
|
|
||||||
// Report progress every fifth loop
|
// 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 });
|
postMessage({ progress: (progress / data.length) * 100 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -6,9 +6,15 @@
|
|||||||
|
|
||||||
<h2 *ngIf="showDevices">Devices</h2>
|
<h2 *ngIf="showDevices">Devices</h2>
|
||||||
<ul class="phoneListing" *ngIf="showDevices">
|
<ul class="phoneListing" *ngIf="showDevices">
|
||||||
<li *ngFor="let phone of this.api.phones">
|
<li class="singlePhone" *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>
|
<img src="assets/phone.svg">
|
||||||
<p>{{phone.modelName}}</p>
|
<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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '../../styles.scss';
|
@import "../../styles.scss";
|
||||||
|
|
||||||
#user {
|
#user {
|
||||||
min-width: 40rem;
|
min-width: 40rem;
|
||||||
@@ -20,16 +20,53 @@
|
|||||||
|
|
||||||
.phoneListing {
|
.phoneListing {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 1.5rem;
|
padding: 1rem;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background-color: $darker;
|
background-color: $darker;
|
||||||
}
|
}
|
||||||
|
|
||||||
.offline {
|
.singlePhone {
|
||||||
color: #ff6464
|
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-weight: lighter;
|
||||||
font-size: 10pt;
|
font-size: 10pt;
|
||||||
}
|
}
|
||||||
6
frontend/src/assets/phone.svg
Normal file
6
frontend/src/assets/phone.svg
Normal 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
1
node_modules
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
frontend/node_modules
|
||||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"requires": true,
|
|
||||||
"lockfileVersion": 1,
|
|
||||||
"dependencies": {
|
|
||||||
"angular-font-awesome": {
|
|
||||||
"version": "3.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/angular-font-awesome/-/angular-font-awesome-3.1.2.tgz",
|
|
||||||
"integrity": "sha1-k3hzJhLY6MceDXwvqg+t3H+Fjsk="
|
|
||||||
},
|
|
||||||
"font-awesome": {
|
|
||||||
"version": "4.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
|
|
||||||
"integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user