Compare commits
3 Commits
feature/re
...
19b7c05d75
| Author | SHA1 | Date | |
|---|---|---|---|
|
19b7c05d75
|
|||
|
28e85ea730
|
|||
|
9f1aa50681
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -107,4 +107,3 @@ dist
|
|||||||
# Stores VSCode versions used for testing VSCode extensions
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
.vscode-test
|
.vscode-test
|
||||||
|
|
||||||
android_new
|
|
||||||
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'
|
||||||
@@ -44,9 +52,8 @@ dependencies {
|
|||||||
implementation 'com.squareup.moshi:moshi:1.11.0'
|
implementation 'com.squareup.moshi:moshi:1.11.0'
|
||||||
implementation 'com.squareup.moshi:moshi-kotlin:1.11.0'
|
implementation 'com.squareup.moshi:moshi-kotlin:1.11.0'
|
||||||
implementation 'com.rabbitmq:amqp-client:5.9.0'
|
implementation 'com.rabbitmq:amqp-client:5.9.0'
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
|
implementation "com.squareup.okhttp3:okhttp:4.9.0"
|
||||||
implementation 'com.squareup.okhttp3:okhttp-sse:4.9.1'
|
|
||||||
testImplementation 'junit:junit:4.+'
|
testImplementation 'junit:junit:4.+'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.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" />
|
||||||
|
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
package de.nicolasklier.livebeat
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.internal.platform.Platform
|
|
||||||
import okhttp3.internal.platform.Platform.Companion.INFO
|
|
||||||
import okhttp3.sse.EventSource
|
|
||||||
import okhttp3.sse.EventSourceListener
|
|
||||||
import okio.IOException
|
|
||||||
import org.jetbrains.annotations.Nullable
|
|
||||||
import java.util.concurrent.BlockingQueue
|
|
||||||
import java.util.concurrent.LinkedBlockingDeque
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
|
|
||||||
class EventSourceRecorder : EventSourceListener() {
|
|
||||||
private val events: BlockingQueue<Any> = LinkedBlockingDeque()
|
|
||||||
|
|
||||||
override fun onOpen(eventSource: EventSource, response: Response) {
|
|
||||||
Log.i("SSE", "Connection opened!")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onEvent(
|
|
||||||
eventSource: EventSource, @Nullable id: String?, @Nullable type: String?,
|
|
||||||
data: String
|
|
||||||
) {
|
|
||||||
Log.i("SSE", "onEvent: " + data)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClosed(eventSource: EventSource) {
|
|
||||||
Log.i("SSE", "Connection to SSE got closed!")
|
|
||||||
events.add(Closed())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onFailure(
|
|
||||||
eventSource: EventSource?,
|
|
||||||
@Nullable t: Throwable
|
|
||||||
) {
|
|
||||||
Platform.get().log("[ES] onFailure", INFO, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun nextEvent(): Any {
|
|
||||||
return try {
|
|
||||||
val event: Any = events.poll(10, TimeUnit.SECONDS)
|
|
||||||
?: throw AssertionError("Timed out waiting for event.")
|
|
||||||
event
|
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
throw AssertionError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class Open(val eventSource: EventSource?, response: Response) {
|
|
||||||
val response: Response
|
|
||||||
override fun toString(): String {
|
|
||||||
return "Open[$response]"
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
this.response = response
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class Failure(val t: Throwable, response: Response?) {
|
|
||||||
val response: Response?
|
|
||||||
val responseBody: String?
|
|
||||||
override fun toString(): String {
|
|
||||||
return if (response == null) {
|
|
||||||
"Failure[$t]"
|
|
||||||
} else "Failure[$response]"
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
this.response = response
|
|
||||||
var responseBody: String? = null
|
|
||||||
if (response != null) {
|
|
||||||
try {
|
|
||||||
responseBody = response.body.toString()
|
|
||||||
} catch (ignored: IOException) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.responseBody = responseBody
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class Closed {
|
|
||||||
override fun toString(): String {
|
|
||||||
return "Closed[]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package de.nicolasklier.livebeat
|
|
||||||
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.sse.EventSource
|
|
||||||
import okhttp3.sse.EventSources
|
|
||||||
import java.lang.Error
|
|
||||||
import java.net.ConnectException
|
|
||||||
|
|
||||||
class HttpRequests {
|
|
||||||
companion object {
|
|
||||||
fun get(url: String, sendToken: Boolean = true): Response {
|
|
||||||
val client = OkHttpClient()
|
|
||||||
var req = Request.Builder()
|
|
||||||
.url(url)
|
|
||||||
.get()
|
|
||||||
.build()
|
|
||||||
|
|
||||||
if (sendToken) {
|
|
||||||
req = req.newBuilder().addHeader("token", MainActivity.TOKEN).build();
|
|
||||||
}
|
|
||||||
return client.newCall(req).execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
fun post(url: String, body: String, sendToken: Boolean = true): Response {
|
|
||||||
try {
|
|
||||||
val client = OkHttpClient()
|
|
||||||
var req = Request.Builder()
|
|
||||||
.url(url)
|
|
||||||
.post(
|
|
||||||
(body).toRequestBody()
|
|
||||||
)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
if (sendToken) {
|
|
||||||
req = req.newBuilder().addHeader("token", MainActivity.TOKEN).build()
|
|
||||||
}
|
|
||||||
return client.newCall(req).execute()
|
|
||||||
} catch (e: ConnectException) {
|
|
||||||
throw Error("Connection to $url couldn't be made: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sse(url: String) {
|
|
||||||
val client = OkHttpClient()
|
|
||||||
val req = Request.Builder().url(url).build();
|
|
||||||
|
|
||||||
val handler = EventSourceRecorder()
|
|
||||||
val factory = EventSources.createFactory(client)
|
|
||||||
val sse = factory.newEventSource(req, handler)
|
|
||||||
|
|
||||||
sse.request()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,17 +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 android.widget.Toast
|
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
|
||||||
@@ -28,12 +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.net.ConnectException
|
|
||||||
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() {
|
||||||
@@ -48,16 +41,25 @@ 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)
|
||||||
/*val client = OkHttpClient()
|
val client = OkHttpClient()
|
||||||
val req = Request.Builder()
|
val req = Request.Builder()
|
||||||
.url("$API_URL/phone/$androidId")
|
.url("$API_URL/phone/$androidId")
|
||||||
.header("token", TOKEN)
|
.header("token", TOKEN)
|
||||||
.get()
|
.get()
|
||||||
.build()*/
|
.build()
|
||||||
val response = HttpRequests.get("$API_URL/phone/$androidId")
|
val response = client.newCall(req).execute()
|
||||||
|
|
||||||
if (response.code != 200) {
|
if (response.code != 200) {
|
||||||
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Device isn't registered yet. Registering ...", Snackbar.LENGTH_SHORT)
|
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Device isn't registered yet. Registering ...", Snackbar.LENGTH_SHORT)
|
||||||
@@ -77,7 +79,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val phoneToJson = moshi.adapter(Phone::class.java)
|
val phoneToJson = moshi.adapter(Phone::class.java)
|
||||||
val json = phoneToJson.toJson(phone)
|
val json = phoneToJson.toJson(phone)
|
||||||
|
|
||||||
/*val createPhone = Request.Builder()
|
val createPhone = Request.Builder()
|
||||||
.url("$API_URL/phone")
|
.url("$API_URL/phone")
|
||||||
.post(
|
.post(
|
||||||
(json).toRequestBody()
|
(json).toRequestBody()
|
||||||
@@ -85,8 +87,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.header("token", TOKEN)
|
.header("token", TOKEN)
|
||||||
.build()
|
.build()
|
||||||
client.newCall(createPhone).execute()*/
|
client.newCall(createPhone).execute()
|
||||||
HttpRequests.post("$API_URL/phone", json);
|
|
||||||
}
|
}
|
||||||
}).start()
|
}).start()
|
||||||
}
|
}
|
||||||
@@ -98,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
|
||||||
@@ -107,40 +106,15 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
|
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
|
||||||
val jsonToLogin = moshi.adapter(Login::class.java)
|
val jsonToLogin = moshi.adapter(Login::class.java)
|
||||||
|
|
||||||
val token = "{ \"username\": \"" + username + "\"," +
|
|
||||||
"\"password\": \"" + password + "\" }"
|
|
||||||
try {
|
|
||||||
HttpRequests.post("$API_URL/user/login", token)
|
|
||||||
} catch (e: Error) {
|
|
||||||
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Backend server is not available", Snackbar.LENGTH_SHORT)
|
|
||||||
.setBackgroundTint(Color.RED)
|
|
||||||
.setActionTextColor(Color.WHITE)
|
|
||||||
.show();
|
|
||||||
return@Runnable
|
|
||||||
}
|
|
||||||
val client = OkHttpClient()
|
val client = OkHttpClient()
|
||||||
val req = Request.Builder()
|
val req = Request.Builder()
|
||||||
.url("$API_URL/user/login")
|
.url("$API_URL/user/login")
|
||||||
.post(
|
.post(
|
||||||
(token).toRequestBody()
|
("{ \"username\": \"" + username + "\"," +
|
||||||
|
"\"password\": \"" + password + "\" }").toRequestBody()
|
||||||
)
|
)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
// Check if server is available.
|
|
||||||
try {
|
|
||||||
val testReq = Request.Builder()
|
|
||||||
.url("$API_URL/user/login")
|
|
||||||
.post(
|
|
||||||
("{ \"username\": \"" + username + "\"," +
|
|
||||||
"\"password\": \"" + password + "\" }").toRequestBody()
|
|
||||||
)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.build()
|
|
||||||
client.newCall(testReq).execute();
|
|
||||||
} catch (e: ConnectException) {
|
|
||||||
}
|
|
||||||
|
|
||||||
val loginResponse = client.newCall(req).execute()
|
val loginResponse = client.newCall(req).execute()
|
||||||
val responseBody = loginResponse.body!!.string()
|
val responseBody = loginResponse.body!!.string()
|
||||||
|
|
||||||
@@ -169,10 +143,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val userInfoResponseBody = userinfoResponse.body!!.string()
|
val userInfoResponseBody = userinfoResponse.body!!.string()
|
||||||
USER = jsonToUser.fromJson(userInfoResponseBody)
|
USER = jsonToUser.fromJson(userInfoResponseBody)
|
||||||
|
|
||||||
val intent = Intent(this, TrackerService::class.java)
|
|
||||||
|
|
||||||
// Only start service if authentication went good.
|
// Only start service if authentication went good.
|
||||||
startService(intent)
|
// 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)
|
||||||
@@ -201,6 +173,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
this.broadcastReceiver = object : BroadcastReceiver() {
|
this.broadcastReceiver = object : BroadcastReceiver() {
|
||||||
@SuppressLint("CutPasteId")
|
@SuppressLint("CutPasteId")
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val statusRabbit = intent.getBooleanExtra("statusRabbit", false)
|
||||||
val statusHttp = intent.getIntExtra("statusHttp", 404)
|
val statusHttp = intent.getIntExtra("statusHttp", 404)
|
||||||
|
|
||||||
/*if (statusHttp == 200) {
|
/*if (statusHttp == 200) {
|
||||||
@@ -224,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 eventToken: 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,19 +10,33 @@ import android.content.pm.PackageManager
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import android.os.BatteryManager
|
import android.os.BatteryManager
|
||||||
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
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.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.Request
|
||||||
|
import java.io.IOException
|
||||||
|
import java.lang.Exception
|
||||||
|
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)
|
||||||
|
|
||||||
@@ -30,9 +44,50 @@ class TrackerService : Service() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("CheckResult")
|
private fun connectSocket() {
|
||||||
private fun subscribeToEvents() {
|
// This thread only connects to RabbitMQ
|
||||||
HttpRequests.sse("http://192.168.178.26/user/events?token=${MainActivity.USER?.eventToken}")
|
val connectionThread = Thread(Runnable {
|
||||||
|
val socket: Socket;
|
||||||
|
|
||||||
|
// Connection values
|
||||||
|
val options = IO.Options.builder()
|
||||||
|
.setTransports(arrayOf("websocket"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try {
|
||||||
|
socket = IO.socket("http://192.168.178.26:8040", options)
|
||||||
|
socket.connect()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("Socket.io", "Unable to connect to socket: ${e.message}")
|
||||||
|
return@Runnable
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on("test") { args ->
|
||||||
|
run {
|
||||||
|
Log.i("Socket.io", args[0].toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on("connect") { args ->
|
||||||
|
run {
|
||||||
|
isSocketConnected = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on("disconnect") { args ->
|
||||||
|
run {
|
||||||
|
isSocketConnected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socket.connected()) {
|
||||||
|
socket.emit("test", "This is a message from my phone!")
|
||||||
|
Log.i("Socket.io", "Published test message")
|
||||||
|
} else {
|
||||||
|
Log.e("Socket.io", "Cannot send test message because I'm not connected with the Socket.io server.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
connectionThread.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("HardwareIds")
|
@SuppressLint("HardwareIds")
|
||||||
@@ -70,13 +125,15 @@ class TrackerService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribeToEvents()
|
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,
|
||||||
@@ -86,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,12 +64,6 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="horizontal"/>
|
|
||||||
|
|
||||||
<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
|
||||||
|
|||||||
8
backend/.idea/.gitignore
generated
vendored
8
backend/.idea/.gitignore
generated
vendored
@@ -1,8 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
8
backend/.idea/backend.iml
generated
8
backend/.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>
|
|
||||||
4
backend/.idea/misc.xml
generated
4
backend/.idea/misc.xml
generated
@@ -1,4 +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" />
|
|
||||||
</project>
|
|
||||||
8
backend/.idea/modules.xml
generated
8
backend/.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/.idea/vcs.xml
generated
6
backend/.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>
|
|
||||||
@@ -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, LoginUser, MW_User, PatchUser, PostUser, UserEvents } 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 { EventManager } from './lib/eventManager';
|
import { SocketManager } from './lib/socketio';
|
||||||
import { RabbitMQ } from './lib/rabbit';
|
|
||||||
import { UserType } from './models/user/user.interface';
|
import { UserType } from './models/user/user.interface';
|
||||||
import { User } from './models/user/user.model';
|
import { User } from './models/user/user.model';
|
||||||
|
|
||||||
@@ -27,8 +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;
|
|
||||||
export let eventManager: EventManager = new EventManager();
|
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
const { combine, timestamp, label, printf, prettyPrint } = winston.format;
|
const { combine, timestamp, label, printf, prettyPrint } = winston.format;
|
||||||
@@ -109,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()),
|
||||||
eventToken: randomString(16),
|
|
||||||
salt,
|
salt,
|
||||||
createdAt: Date.now(),
|
lastLogin: new Date(0),
|
||||||
lastLogin: 0,
|
|
||||||
type: UserType.ADMIN
|
type: UserType.ADMIN
|
||||||
});
|
});
|
||||||
logger.info("===================================================");
|
logger.info("===================================================");
|
||||||
@@ -125,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();
|
||||||
@@ -140,12 +146,11 @@ async function run() {
|
|||||||
|
|
||||||
app.get('/', (req, res) => res.status(200).send('OK'));
|
app.get('/', (req, res) => res.status(200).send('OK'));
|
||||||
|
|
||||||
// User authentication & actions
|
// User authentication
|
||||||
app.post('/user/login', (req, res) => LoginUser(req, res));
|
app.post('/user/login', (req, res) => LoginUser(req, res));
|
||||||
|
|
||||||
// CRUD user
|
// CRUD user
|
||||||
app.get('/user/notification', MW_User, (req, res) => getNotification(req, res)); // Notifications
|
app.get('/user/notification', MW_User, (req, res) => getNotification(req, res)); // Notifications
|
||||||
app.get('/user/events', (req, res) => UserEvents(req, res));
|
|
||||||
app.get('/user/', MW_User, (req, res) => GetUser(req, res));
|
app.get('/user/', MW_User, (req, res) => GetUser(req, res));
|
||||||
app.post('/user/', MW_User, (req, res) => PostUser(req, res));
|
app.post('/user/', MW_User, (req, res) => PostUser(req, res));
|
||||||
app.get('/user/:id', MW_User, (req, res) => GetUser(req, res));
|
app.get('/user/:id', MW_User, (req, res) => GetUser(req, res));
|
||||||
@@ -161,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();
|
||||||
@@ -16,10 +16,6 @@ export const config: IConfig = {
|
|||||||
host: "0.0.0.0"
|
host: "0.0.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* END OF CONFIG
|
|
||||||
* ====================
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface IConfig {
|
export interface IConfig {
|
||||||
authentification: {
|
authentification: {
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { Response } from "express";
|
import { Response } from "express";
|
||||||
import { eventManager, logger } from "../app";
|
|
||||||
import { LivebeatRequest } from "../lib/request";
|
import { LivebeatRequest } from "../lib/request";
|
||||||
import { IBeat } from "../models/beat/beat.interface";
|
import { IBeat } from "../models/beat/beat.interface";
|
||||||
import { Beat } from "../models/beat/beat.model.";
|
import { Beat } from "../models/beat/beat.model.";
|
||||||
import { ISeverity } from "../models/notifications/notification.interface";
|
|
||||||
import { Phone } from "../models/phone/phone.model";
|
import { Phone } from "../models/phone/phone.model";
|
||||||
|
|
||||||
const timeouts: Map<string, NodeJS.Timeout> = new Map<string, NodeJS.Timeout>();
|
|
||||||
|
|
||||||
export async function GetBeatStats(req: LivebeatRequest, res: Response) {
|
export async function GetBeatStats(req: LivebeatRequest, res: Response) {
|
||||||
const phones = await Phone.find({ user: req.user?._id });
|
const phones = await Phone.find({ user: req.user?._id }).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;
|
||||||
@@ -24,26 +24,23 @@ export async function GetBeatStats(req: LivebeatRequest, res: Response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function GetBeat(req: LivebeatRequest, res: Response) {
|
export async function GetBeat(req: LivebeatRequest, res: Response) {
|
||||||
const from: number = Number(req.query.from || 0);
|
const from: number = Number(req.query.from);
|
||||||
const to: number = Number(req.query.to || Date.now() / 1000);
|
const to: number = Number(req.query.to);
|
||||||
const limit: number = Number(req.query.limit || 10000);
|
const limit: number = Number(req.query.limit || 10000);
|
||||||
const sort: number = Number(req.query.sort || 1); // Either -1 or 1
|
const sort: number = Number(req.query.sort || 1); // Either -1 or 1
|
||||||
const phoneId = req.query.phoneId;
|
const phoneId = req.query.phoneId;
|
||||||
|
|
||||||
// Grab default phone if non was provided.
|
// Grab default phone if non was provided.
|
||||||
const phone = req.query.phone === undefined ? await Phone.findOne({ user: req.user?._id }) : await Phone.findOne({ _id: phoneId, user: req.user?._id });
|
const phone = req.query.phone === undefined ? await Phone.findOne({ user: req.user?._id }) : await Phone.findOne({ _id: phoneId, user: req.user?._id });
|
||||||
let beats: IBeat[] = [];
|
let beats: IBeat[] = []
|
||||||
|
|
||||||
//console.log(from, to);
|
|
||||||
//console.log(`Search from ${new Date(from).toString()} to ${new Date(to * 1000).toString()}`);
|
|
||||||
|
|
||||||
if (phone !== null) {
|
if (phone !== null) {
|
||||||
beats = await Beat.find(
|
beats = await Beat.find(
|
||||||
{
|
{
|
||||||
phone: phone._id,
|
phone: phone._id,
|
||||||
createdAt: {
|
createdAt: {
|
||||||
$gte: new Date((from)),
|
$gte: new Date((from | 0) * 1000),
|
||||||
$lte: new Date(to * 1000)
|
$lte: new Date((to | Date.now() /1000) * 1000)
|
||||||
}
|
}
|
||||||
}).sort({ _id: sort }).limit(limit);
|
}).sort({ _id: sort }).limit(limit);
|
||||||
res.status(200).send(beats);
|
res.status(200).send(beats);
|
||||||
@@ -51,61 +48,3 @@ export async function GetBeat(req: LivebeatRequest, res: Response) {
|
|||||||
res.status(404).send({ message: 'Phone not found' });
|
res.status(404).send({ message: 'Phone not found' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function AddBeat(req: LivebeatRequest, res: Response) {
|
|
||||||
const beat = req.body as IBeat;
|
|
||||||
const androidId = req.headers.deviceId as string;
|
|
||||||
|
|
||||||
if (androidId === undefined) {
|
|
||||||
res.status(401).send({ message: 'Device id is missing' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get phone
|
|
||||||
const phone = await Phone.findOne({ androidId });
|
|
||||||
if (phone == undefined) {
|
|
||||||
logger.warning(`Received beat from unknown device with id ${androidId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let newBeat;
|
|
||||||
if (beat.coordinate !== undefined && beat.accuracy !== undefined) {
|
|
||||||
logger.info(`New beat from ${phone.displayName} => ${beat.coordinate[0]}, ${beat.coordinate[1]} | Height: ${beat.coordinate[3]}m | Speed: ${beat.coordinate[4]} | Accuracy: ${beat.accuracy}% | Battery: ${beat.battery}%`);
|
|
||||||
|
|
||||||
newBeat = await Beat.create({
|
|
||||||
phone: phone._id,
|
|
||||||
// [latitude, longitude, altitude]
|
|
||||||
coordinate: [beat.coordinate[0], beat.coordinate[1], beat.coordinate[2]],
|
|
||||||
accuracy: beat.coordinate[3],
|
|
||||||
speed: beat.coordinate[4],
|
|
||||||
battery: beat.battery,
|
|
||||||
createdAt: Date.now()
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
newBeat = await Beat.create({
|
|
||||||
phone: phone._id,
|
|
||||||
battery: beat.battery,
|
|
||||||
createdAt: Date.now()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Broadcast if device became active
|
|
||||||
if (timeouts.has(phone.id)) {
|
|
||||||
clearTimeout(timeouts.get(phone.id)!!);
|
|
||||||
} else {
|
|
||||||
phone.active = true;
|
|
||||||
await phone.save();
|
|
||||||
|
|
||||||
eventManager.push('phone_alive', phone.toJSON(), phone.user);
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeoutTimer = setTimeout(async () => {
|
|
||||||
eventManager.push('phone_dead', phone.toJSON(), phone.user, ISeverity.WARN);
|
|
||||||
timeouts.delete(phone.id);
|
|
||||||
phone.active = false;
|
|
||||||
await phone.save();
|
|
||||||
}, 60_000);
|
|
||||||
timeouts.set(phone.id, timeoutTimer);
|
|
||||||
|
|
||||||
eventManager.push('beat', newBeat.toJSON(), phone.user);
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
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";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function GetPhone(req: LivebeatRequest, res: Response) {
|
export async function GetPhone(req: LivebeatRequest, res: Response) {
|
||||||
const phoneId: String = req.params['id'];
|
const phoneId: String = req.params['id'];
|
||||||
|
|
||||||
@@ -65,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();
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { decode, sign, verify } from 'jsonwebtoken';
|
import { decode, sign, verify } from 'jsonwebtoken';
|
||||||
|
|
||||||
import { eventManager, JWT_SECRET, logger, RABBITMQ_URI } from '../app';
|
import { JWT_SECRET, logger, RABBITMQ_URI } from '../app';
|
||||||
import * as jwt from 'jsonwebtoken';
|
import * as jwt from 'jsonwebtoken';
|
||||||
import { config } from '../config';
|
import { config } from '../config';
|
||||||
import { hashPassword, randomPepper, randomString, verifyPassword } from '../lib/crypto';
|
import { hashPassword, randomPepper, randomString, verifyPassword } from '../lib/crypto';
|
||||||
@@ -51,7 +51,7 @@ export async function PostUser(req: LivebeatRequest, res: Response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const salt = randomString(config.authentification.salt_length);
|
const salt = randomString(config.authentification.salt_length);
|
||||||
const eventToken = randomString(16);
|
const brokerToken = randomString(16);
|
||||||
const hashedPassword = await hashPassword(password + salt + randomPepper()).catch(error => {
|
const hashedPassword = await hashPassword(password + salt + randomPepper()).catch(error => {
|
||||||
res.status(400).send({ message: 'Provided password is too weak and cannot be used.' });
|
res.status(400).send({ message: 'Provided password is too weak and cannot be used.' });
|
||||||
return;
|
return;
|
||||||
@@ -61,7 +61,7 @@ export async function PostUser(req: LivebeatRequest, res: Response) {
|
|||||||
name,
|
name,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
salt,
|
salt,
|
||||||
eventToken,
|
brokerToken,
|
||||||
type,
|
type,
|
||||||
lastLogin: new Date(0)
|
lastLogin: new Date(0)
|
||||||
});
|
});
|
||||||
@@ -72,27 +72,6 @@ export async function PostUser(req: LivebeatRequest, res: Response) {
|
|||||||
res.status(200).send({ setupToken });
|
res.status(200).send({ setupToken });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function UserEvents(req: LivebeatRequest, res: Response) {
|
|
||||||
if (req.query.token === undefined) {
|
|
||||||
res.status(401).send({ message: 'You need to define your event token.' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventToken = req.query.token as string;
|
|
||||||
const user = await User.findOne({ eventToken });
|
|
||||||
|
|
||||||
if (user === null) {
|
|
||||||
res.status(401).send({ message: 'This event token is not valid.' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
eventManager.join(user.id, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function UserSubscribeEvent(req: Request, res: Response) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function DeleteUser(req: Request, res: Response) {
|
export async function DeleteUser(req: Request, res: Response) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -132,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();
|
||||||
|
|||||||
@@ -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,170 +0,0 @@
|
|||||||
import { Response } from "express";
|
|
||||||
import { logger } from "../app";
|
|
||||||
import { ISeverity, NotificationType, PublicNotificationType } from "../models/notifications/notification.interface";
|
|
||||||
import { addNotification } from "../models/notifications/notification.model";
|
|
||||||
import { IPhone } from "../models/phone/phone.interface";
|
|
||||||
import { IUser } from "../models/user/user.interface";
|
|
||||||
import { User } from "../models/user/user.model";
|
|
||||||
import { randomString } from "./crypto";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class stores one specific client.
|
|
||||||
*/
|
|
||||||
export class Client {
|
|
||||||
id: string;
|
|
||||||
userId: string;
|
|
||||||
stream: Response;
|
|
||||||
|
|
||||||
constructor(stream: Response, userId: string) {
|
|
||||||
this.id = randomString(16);
|
|
||||||
this.userId = userId;
|
|
||||||
this.stream = stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
send(type: NotificationType, data: any) {
|
|
||||||
this.stream.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUser() {
|
|
||||||
return await User.findById(this.userId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Clients {
|
|
||||||
private clients: Client[];
|
|
||||||
|
|
||||||
constructor(clients: Client[]) {
|
|
||||||
this.clients = clients;
|
|
||||||
}
|
|
||||||
|
|
||||||
getClientsByUser(userId: string) {
|
|
||||||
const userClients = [];
|
|
||||||
for (let i = 0; i < this.clients.length; i++) {
|
|
||||||
if (this.clients[i].userId === userId) {
|
|
||||||
userClients.push(this.clients[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return userClients;
|
|
||||||
}
|
|
||||||
|
|
||||||
closeAllClientsByUser(userId: string) {
|
|
||||||
this.getClientsByUser(userId).forEach(client => {
|
|
||||||
client.stream.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addClient(client: Client) {
|
|
||||||
this.clients.push(client);
|
|
||||||
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
getClients() {
|
|
||||||
return this.clients;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EventManager {
|
|
||||||
constructor() {
|
|
||||||
setInterval(() => {
|
|
||||||
this.broadcast('info', { message: "Test" });
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This map stores a open data stream and it's associated room.
|
|
||||||
private clients: Clients = new Clients([]);
|
|
||||||
|
|
||||||
private addClient(stream: Response, userId: string) {
|
|
||||||
this.clients.addClient(new Client(stream, userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a client to a specific room
|
|
||||||
* @param room Used as an id for the specific room
|
|
||||||
* @param stream A open connection to the user
|
|
||||||
*/
|
|
||||||
async join(userId: string, stream: Response) {
|
|
||||||
if (stream.req == undefined) {
|
|
||||||
stream.send(500);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check user
|
|
||||||
const user = await User.findById(userId);
|
|
||||||
if (user === null) {
|
|
||||||
stream.send(401);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure to keep the connection open
|
|
||||||
stream.writeHead(200, {
|
|
||||||
'Content-Type': 'text/event-stream',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Connection': 'keep-alive'
|
|
||||||
});
|
|
||||||
|
|
||||||
this.addClient(stream, userId);
|
|
||||||
logger.debug(`Client ${stream.req.hostname} of user ${user.name} joined.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Push a new event into a specific room
|
|
||||||
* @param event Type of the event
|
|
||||||
* @param data Content of the event
|
|
||||||
* @param selector Room to push in. If empty then it will be a public broadcast to anyone.
|
|
||||||
*/
|
|
||||||
push(type: NotificationType, data: any, user: IUser, severity = ISeverity.INFO) {
|
|
||||||
let clients = this.clients.getClientsByUser(user.id);
|
|
||||||
if (clients === undefined) return;
|
|
||||||
|
|
||||||
/* Manage notifications */
|
|
||||||
if (type != 'beat' && user !== undefined) {
|
|
||||||
if (type == 'phone_alive' || type == 'phone_dead') {
|
|
||||||
addNotification(type, severity, ((data as IPhone)._id), user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data = { type, severity, ...data };
|
|
||||||
|
|
||||||
clients.forEach((client) => {
|
|
||||||
client.stream.write(`event: ${type}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (user === undefined) {
|
|
||||||
logger.debug(`Broadcasted event ${type} to all users (${clients.length} clients affected)`);
|
|
||||||
} else {
|
|
||||||
logger.debug(`Broadcasted event ${type} to user ${user.id} (${clients.length} clients affected)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Very much like push() but it will send this message to **every connected client!**
|
|
||||||
*/
|
|
||||||
broadcast(type: PublicNotificationType, data: any) {
|
|
||||||
this.clients.getClients().forEach(async client => {
|
|
||||||
console.log(`Send ${JSON.stringify(data)} (of type ${type}) to a client of user ${(await client.getUser())?.name}`);
|
|
||||||
client.stream.write(`event: message\ndata: ${JSON.stringify(data)}\n\n`);
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.debug(`Broadcasted event ${type} to all users (${this.clients.getClients().length} clients affected)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* End the communication with a specific client.
|
|
||||||
*/
|
|
||||||
end(stream: Response, userId: string) {
|
|
||||||
stream.end();
|
|
||||||
|
|
||||||
logger.debug(`End connection with ${stream.req?.hostname} (user: ${userId})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
static buildEventTypeName(type: EventType, user: IUser) {
|
|
||||||
return `${type}-${user}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EventType =
|
|
||||||
| 'tracker' // Receive just the gps location of a specific user.
|
|
||||||
| 'user' // Receive user updates.
|
|
||||||
| 'all'; // Receive all above events.
|
|
||||||
@@ -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';
|
||||||
@@ -63,7 +61,7 @@ export class RabbitMQ {
|
|||||||
this.timeouts.delete(phone.id);
|
this.timeouts.delete(phone.id);
|
||||||
phone.active = false;
|
phone.active = false;
|
||||||
await phone.save();
|
await phone.save();
|
||||||
}, 60_000);
|
}, 30_000);
|
||||||
this.timeouts.set(phone.id, timeoutTimer);
|
this.timeouts.set(phone.id, timeoutTimer);
|
||||||
|
|
||||||
this.publish(phone.user.toString(), newBeat.toJSON(), 'beat');
|
this.publish(phone.user.toString(), newBeat.toJSON(), 'beat');
|
||||||
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,8 +4,8 @@ import { IPhone } from '../phone/phone.interface';
|
|||||||
export interface IBeat extends Document {
|
export interface IBeat extends Document {
|
||||||
// [latitude, longitude, altitude, accuracy, speed]
|
// [latitude, longitude, altitude, accuracy, speed]
|
||||||
coordinate?: number[],
|
coordinate?: number[],
|
||||||
accuracy?: number,
|
accuracy: number,
|
||||||
speed?: number,
|
speed: number,
|
||||||
battery?: number,
|
battery?: number,
|
||||||
phone: IPhone,
|
phone: IPhone,
|
||||||
createdAt?: Date
|
createdAt?: Date
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ export enum ISeverity {
|
|||||||
ERROR = 3
|
ERROR = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NotificationType = 'beat' | 'phone_alive' | 'phone_dead' | 'phone_register' | 'panic' | 'test';
|
export type NotificationType = 'beat' | 'phone_alive' | 'phone_dead' | 'phone_register' | 'panic';
|
||||||
export type PublicNotificationType = 'shutdown' | 'restart' | 'warning' | 'error' | 'info';
|
|
||||||
|
|
||||||
export interface INotification extends Document {
|
export interface INotification extends Document {
|
||||||
type: NotificationType;
|
type: NotificationType;
|
||||||
|
|||||||
@@ -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
|
||||||
eventToken: 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 },
|
||||||
eventToken: { type: String, required: true },
|
|
||||||
lastLogin: { type: Date, required: true, default: Date.now },
|
lastLogin: { type: Date, required: true, default: Date.now },
|
||||||
}, {
|
}, {
|
||||||
timestamps: {
|
timestamps: {
|
||||||
|
|||||||
4041
backend/package-lock.json
generated
4041
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -17,10 +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",
|
|
||||||
"bi-directional-map": "^1.0.0",
|
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
@@ -29,13 +26,13 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/amqplib": "0.5.14",
|
|
||||||
"@types/argon2": "0.15.0",
|
"@types/argon2": "0.15.0",
|
||||||
"@types/body-parser": "1.19.0",
|
"@types/body-parser": "1.19.0",
|
||||||
"@types/chalk": "2.2.0",
|
"@types/chalk": "2.2.0",
|
||||||
@@ -46,6 +43,8 @@
|
|||||||
"@types/jsonwebtoken": "8.5.0",
|
"@types/jsonwebtoken": "8.5.0",
|
||||||
"@types/moment": "2.13.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",
|
||||||
|
|||||||
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
|
|
||||||
27298
frontend/package-lock.json
generated
27298
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,19 +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-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",
|
||||||
@@ -51,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 {
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { Injectable } from '@angular/core';
|
|||||||
import { BehaviorSubject } from 'rxjs';
|
import { BehaviorSubject } from 'rxjs';
|
||||||
import * as moment from 'moment';
|
import * as moment from 'moment';
|
||||||
import { AlertService, AlertType } from './_alert/alert.service';
|
import { AlertService, AlertType } from './_alert/alert.service';
|
||||||
|
import { Socket } from 'ngx-socket-io';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ==========================
|
|
||||||
* DEFINITION OF TYPE
|
* DEFINITION OF TYPE
|
||||||
*/
|
*/
|
||||||
export interface ILogin {
|
export interface ILogin {
|
||||||
@@ -43,7 +43,7 @@ export enum UserType {
|
|||||||
export interface IUser {
|
export interface IUser {
|
||||||
_id: string;
|
_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
eventToken: string;
|
brokerToken: string;
|
||||||
type: UserType;
|
type: UserType;
|
||||||
lastLogin: Date;
|
lastLogin: Date;
|
||||||
twoFASecret?: string;
|
twoFASecret?: string;
|
||||||
@@ -57,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;
|
||||||
@@ -83,13 +87,8 @@ export interface INotification extends Document {
|
|||||||
user: IUser;
|
user: IUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CustomEvent extends Event {
|
|
||||||
data: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* END OF THE TYPE DEFINITION
|
* END OF THE DEFINITION OF TYPE
|
||||||
* ==========================
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@@ -98,9 +97,9 @@ interface CustomEvent extends Event {
|
|||||||
export class APIService {
|
export class APIService {
|
||||||
|
|
||||||
private token: string;
|
private token: string;
|
||||||
private events: EventSource | undefined;
|
|
||||||
|
|
||||||
username: string;
|
username: string;
|
||||||
|
rabbitmq: any;
|
||||||
|
|
||||||
// Passthough data (not useful for api but a way for components to share data)
|
// Passthough data (not useful for api but a way for components to share data)
|
||||||
showFilter = true;
|
showFilter = true;
|
||||||
@@ -116,7 +115,7 @@ export class APIService {
|
|||||||
user: IUser = {
|
user: IUser = {
|
||||||
_id: '',
|
_id: '',
|
||||||
name: '',
|
name: '',
|
||||||
eventToken: '',
|
brokerToken: '',
|
||||||
lastLogin: new Date(2020, 3, 1),
|
lastLogin: new Date(2020, 3, 1),
|
||||||
type: UserType.GUEST,
|
type: UserType.GUEST,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -130,68 +129,60 @@ 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 alert: AlertService) { }
|
constructor(private httpClient: HttpClient, private alert: AlertService, private socket: Socket) { }
|
||||||
|
|
||||||
/**
|
private socketInit(): void {
|
||||||
* This functions opens a new http connection that stays open, forever, to receive events from the backend.
|
// Connect with Socket.io after we received our user information
|
||||||
*/
|
this.socket.connect();
|
||||||
async subscribeToEvents() {
|
this.socket.on('connect', () => {
|
||||||
let shownError = false;
|
console.log("HERE: " + this.token);
|
||||||
|
|
||||||
// If there is already a event, close it.
|
this.socket.emit('loginFrontend', this.token);
|
||||||
if (this.events !== undefined) {
|
});
|
||||||
this.events.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.events = new EventSource(`${this.API_ENDPOINT}/user/events?token=${this.user.eventToken}`);
|
this.socket.on('test', data => {
|
||||||
|
this.alert.info(data);
|
||||||
|
console.log('Received test:', data);
|
||||||
|
});
|
||||||
|
|
||||||
this.events.onopen = event => {
|
this.socket.on('approvePhone', (phone: IPhone) => {
|
||||||
console.info('Connection to event stream is open. Awaiting incoming events ...');
|
this.alert.dynamic(`To pair ${phone.displayName}, type in code: ${phone.approval.code}`, AlertType.INFO, 'Pair', { duration: 0 });
|
||||||
shownError = false;
|
});
|
||||||
};
|
|
||||||
|
|
||||||
this.events.onerror = error => {
|
/*this.mqtt.connect({
|
||||||
if (shownError) return;
|
hostname: '192.168.178.26',
|
||||||
console.error('Connection to event stream has failed! Error: ' + error);
|
port: 15675,
|
||||||
this.alert.error('Could not subscribe to events', 'Events');
|
protocol: 'ws',
|
||||||
|
path: '/ws',
|
||||||
|
username: this.user.name,
|
||||||
|
password: this.user.brokerToken
|
||||||
|
});
|
||||||
|
|
||||||
shownError = true;
|
this.mqtt.observe(this.user._id).subscribe(async message => {
|
||||||
}
|
if (message !== undefined || message !== null) {
|
||||||
|
const obj = JSON.parse(message.payload.toString());
|
||||||
|
console.log('Received message:', obj);
|
||||||
|
|
||||||
this.events.onmessage = async event => {
|
if (obj.type === 'beat') {
|
||||||
const jsonData = JSON.parse(event.data);
|
|
||||||
console.debug(`[SSE] ${event.type}: ${event.data}`);
|
|
||||||
|
|
||||||
switch (event.type) {
|
|
||||||
case 'beat':
|
|
||||||
if (this.beats !== undefined) {
|
if (this.beats !== undefined) {
|
||||||
this.beats.push(jsonData);
|
this.beats.push(obj);
|
||||||
this.beatsEvent.next([jsonData]); // We just push one, so the map doesn't has to rebuild everything from scratch.
|
this.beatsEvent.next([obj]); // We just push one, so the map doesn't has to rebuild everything from scratch.
|
||||||
this.beatStats.totalBeats++;
|
this.beatStats.totalBeats++;
|
||||||
}
|
}
|
||||||
|
} else if (obj.type === 'phone_available') {
|
||||||
console.debug('Received count:', jsonData);
|
this.alert.dynamic(`Device ${obj.displayName} is now online`, obj.severity, 'Device');
|
||||||
break;
|
} else if (obj.type === 'phone_register') {
|
||||||
case 'message':
|
|
||||||
this.alert.info(event.data.message, 'SSE');
|
|
||||||
break;
|
|
||||||
case 'phone_available':
|
|
||||||
this.alert.dynamic(`Device ${jsonData.displayName} is now online`, jsonData.severity, 'Device');
|
|
||||||
break;
|
|
||||||
case 'phone_register':
|
|
||||||
await this.getPhones();
|
await this.getPhones();
|
||||||
this.alert.dynamic(`New device "${jsonData.displayName}"`, jsonData.severity, 'New device');
|
this.alert.dynamic(`New device "${obj.displayName}"`, obj.severity, 'New device');
|
||||||
break;
|
} else if (obj.type === 'phone_alive') {
|
||||||
case 'phone_alive':
|
this.alert.dynamic('Device is now active', obj.severity, obj.displayName);
|
||||||
this.alert.dynamic('Device is now active', jsonData.severity, jsonData.displayName);
|
} else if (obj.type === 'phone_dead') {
|
||||||
break;
|
this.alert.dynamic('Device is now offline', obj.severity, obj.displayName);
|
||||||
case 'phone_dead':
|
}
|
||||||
this.alert.dynamic('Device is now offline', jsonData.severity, jsonData.displayName);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
};
|
});*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -199,7 +190,6 @@ export class APIService {
|
|||||||
*/
|
*/
|
||||||
async login(username: string, password: string): Promise<ILogin> {
|
async login(username: string, password: string): Promise<ILogin> {
|
||||||
return new Promise<ILogin>(async (resolve, reject) => {
|
return new Promise<ILogin>(async (resolve, reject) => {
|
||||||
if (this.token !== undefined) reject('User is already logged in.');
|
|
||||||
this.httpClient.post(this.API_ENDPOINT + '/user/login', { username, password }, { responseType: 'json' })
|
this.httpClient.post(this.API_ENDPOINT + '/user/login', { username, password }, { responseType: 'json' })
|
||||||
.subscribe(async token => {
|
.subscribe(async token => {
|
||||||
console.log(token);
|
console.log(token);
|
||||||
@@ -210,9 +200,9 @@ export class APIService {
|
|||||||
await this.getUserInfo();
|
await this.getUserInfo();
|
||||||
await this.getNotifications();
|
await this.getNotifications();
|
||||||
|
|
||||||
this.subscribeToEvents();
|
this.socketInit();
|
||||||
|
|
||||||
await this.getBeats({ from: moment().startOf('day').unix(), to: moment().unix() });
|
await this.getBeats();
|
||||||
await this.getBeatStats();
|
await this.getBeatStats();
|
||||||
this.loginEvent.next(true);
|
this.loginEvent.next(true);
|
||||||
this.alert.success('Login successful', 'Login', { duration: 2 });
|
this.alert.success('Login successful', 'Login', { duration: 2 });
|
||||||
@@ -245,9 +235,9 @@ export class APIService {
|
|||||||
|
|
||||||
resolve(res.setupToken);
|
resolve(res.setupToken);
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +404,7 @@ export class APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* HELPER FUNCTIONS */
|
/* HELPER CLASSES */
|
||||||
degreesToRadians(degrees: number): number {
|
degreesToRadians(degrees: number): number {
|
||||||
return degrees * Math.PI / 180;
|
return degrees * Math.PI / 180;
|
||||||
}
|
}
|
||||||
@@ -458,14 +448,7 @@ export class APIService {
|
|||||||
return this.beats[this.beats.length - 1];
|
return this.beats[this.beats.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns `true` if user is logged in and `false` if not.
|
|
||||||
*/
|
|
||||||
hasSession(): boolean {
|
hasSession(): boolean {
|
||||||
return this.token !== undefined;
|
return this.token !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
getToken(): string {
|
|
||||||
return this.token;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
@@ -20,7 +20,12 @@ import { AdminComponent } from './admin/admin.component';
|
|||||||
import { AlertComponent } from './_alert/alert/alert.component';
|
import { AlertComponent } from './_alert/alert/alert.component';
|
||||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||||
import { NotificationsComponent } from './notifications/notifications.component';
|
import { NotificationsComponent } from './notifications/notifications.component';
|
||||||
import { BackendInterceptor } from './interceptor';
|
import { SocketIoConfig, SocketIoModule } from 'ngx-socket-io';
|
||||||
|
|
||||||
|
const config: SocketIoConfig = { url: 'http://localhost:8040', options: {
|
||||||
|
transports: ['websocket'],
|
||||||
|
autoConnect: false
|
||||||
|
}};
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -47,15 +52,10 @@ import { BackendInterceptor } from './interceptor';
|
|||||||
}),
|
}),
|
||||||
ChartsModule,
|
ChartsModule,
|
||||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
||||||
FontAwesomeModule
|
FontAwesomeModule,
|
||||||
],
|
SocketIoModule.forRoot(config)
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: HTTP_INTERCEPTORS,
|
|
||||||
useClass: BackendInterceptor,
|
|
||||||
multi: true
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
|
providers: [],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
export class AppModule { }
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { HttpInterceptor, HttpEvent, HttpResponse, HttpRequest, HttpHandler } from '@angular/common/http';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { APIService } from './api.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class BackendInterceptor implements HttpInterceptor {
|
|
||||||
|
|
||||||
constructor (private api: APIService) {}
|
|
||||||
|
|
||||||
intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
|
||||||
if (this.api.hasSession()) {
|
|
||||||
console.debug('Inject token for', httpRequest.url);
|
|
||||||
return next.handle(httpRequest.clone({ setHeaders: { token: this.api.getToken() } }));
|
|
||||||
}
|
|
||||||
|
|
||||||
return next.handle(httpRequest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<mgl-map [style]="'mapbox://styles/mapbox/outdoors-v11'" [zoom]="[15]" [center]="[this.lastLocation[0], this.lastLocation[1]]" *ngIf="showMap">
|
<mgl-map [style]="'mapbox://styles/mapbox/dark-v10'" [zoom]="[15]" [center]="[this.lastLocation[0], this.lastLocation[1]]" *ngIf="showMap">
|
||||||
<mgl-geojson-source id="locHistory" [data]="data"></mgl-geojson-source>
|
<mgl-geojson-source id="locHistory" [data]="data"></mgl-geojson-source>
|
||||||
<mgl-geojson-source id="locHistoryFiltered" [data]="mostVisitData"></mgl-geojson-source>
|
<mgl-geojson-source id="locHistoryFiltered" [data]="mostVisitData"></mgl-geojson-source>
|
||||||
<mgl-geojson-source id="lastLoc" [data]="lastLocationData"></mgl-geojson-source>
|
<mgl-geojson-source id="lastLoc" [data]="lastLocationData"></mgl-geojson-source>
|
||||||
|
|||||||
@@ -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 |
165
logo2.svg
165
logo2.svg
@@ -1,165 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
width="512"
|
|
||||||
height="512"
|
|
||||||
viewBox="0 0 135.46666 135.46667"
|
|
||||||
version="1.1"
|
|
||||||
id="svg8"
|
|
||||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
|
|
||||||
sodipodi:docname="logo2.svg">
|
|
||||||
<defs
|
|
||||||
id="defs2">
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
id="linearGradient894">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#ea0202;stop-opacity:0.21047072"
|
|
||||||
offset="0"
|
|
||||||
id="stop890" />
|
|
||||||
<stop
|
|
||||||
style="stop-color:#ea0202;stop-opacity:0"
|
|
||||||
offset="1"
|
|
||||||
id="stop892" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
id="linearGradient882">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#ea0202;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop878" />
|
|
||||||
<stop
|
|
||||||
style="stop-color:#ea0202;stop-opacity:0;"
|
|
||||||
offset="1"
|
|
||||||
id="stop880" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
id="linearGradient866">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#ea0202;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop862" />
|
|
||||||
<stop
|
|
||||||
style="stop-color:#ea0202;stop-opacity:0;"
|
|
||||||
offset="1"
|
|
||||||
id="stop864" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient866"
|
|
||||||
id="linearGradient868"
|
|
||||||
x1="12.343376"
|
|
||||||
y1="22.496964"
|
|
||||||
x2="12.343376"
|
|
||||||
y2="-0.6001001"
|
|
||||||
gradientUnits="userSpaceOnUse" />
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient882"
|
|
||||||
id="linearGradient884"
|
|
||||||
x1="16"
|
|
||||||
y1="8"
|
|
||||||
x2="6.598474"
|
|
||||||
y2="17.448318"
|
|
||||||
gradientUnits="userSpaceOnUse" />
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient894"
|
|
||||||
id="linearGradient888"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
x1="21.759228"
|
|
||||||
y1="2.2183397"
|
|
||||||
x2="0.69895685"
|
|
||||||
y2="21.501596"
|
|
||||||
gradientTransform="matrix(0.80230116,0,0,0.80230116,2.3723861,2.3723861)" />
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient894"
|
|
||||||
id="linearGradient898"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
gradientTransform="matrix(0.52333779,0,0,0.52333779,-18.280053,5.7199465)"
|
|
||||||
x1="21.759228"
|
|
||||||
y1="2.2183397"
|
|
||||||
x2="0.69895685"
|
|
||||||
y2="21.501596" />
|
|
||||||
</defs>
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="base"
|
|
||||||
pagecolor="#292929"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:zoom="1.4"
|
|
||||||
inkscape:cx="198.97429"
|
|
||||||
inkscape:cy="322.59346"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
inkscape:current-layer="g860"
|
|
||||||
inkscape:document-rotation="0"
|
|
||||||
showgrid="false"
|
|
||||||
units="px"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
inkscape:window-height="1027"
|
|
||||||
inkscape:window-x="1920"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1" />
|
|
||||||
<metadata
|
|
||||||
id="metadata5">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
inkscape:label="Ebene 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1">
|
|
||||||
<g
|
|
||||||
style="fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"
|
|
||||||
id="g860"
|
|
||||||
transform="scale(5.6666667)">
|
|
||||||
<path
|
|
||||||
stroke="none"
|
|
||||||
d="M 0,0 H 24 V 24 H 0 Z"
|
|
||||||
fill="none"
|
|
||||||
id="path843" />
|
|
||||||
<circle
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="9"
|
|
||||||
id="circle845"
|
|
||||||
style="stroke:url(#linearGradient868);stroke-opacity:1;stroke-width:0.88235294;stroke-miterlimit:4;stroke-dasharray:none" />
|
|
||||||
<path
|
|
||||||
d="M 12,17 11,13 7,12 16,8 Z"
|
|
||||||
id="path847"
|
|
||||||
style="stroke:url(#linearGradient884);stroke-opacity:1;stroke-width:0.88235294;stroke-miterlimit:4;stroke-dasharray:none;stroke-linecap:butt" />
|
|
||||||
<circle
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="7.2207103"
|
|
||||||
id="circle886"
|
|
||||||
style="stroke:url(#linearGradient888);stroke-width:0.707913;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
|
||||||
<circle
|
|
||||||
cx="-12"
|
|
||||||
cy="12"
|
|
||||||
r="4.7100401"
|
|
||||||
id="circle896"
|
|
||||||
style="stroke:url(#linearGradient898);stroke-width:0.461769;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
transform="rotate(-90)" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.9 KiB |
1
node_modules
Symbolic link
1
node_modules
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
frontend/node_modules
|
||||||
272
package-lock.json
generated
272
package-lock.json
generated
@@ -1,272 +0,0 @@
|
|||||||
{
|
|
||||||
"requires": true,
|
|
||||||
"lockfileVersion": 1,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/bson": {
|
|
||||||
"version": "4.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.3.tgz",
|
|
||||||
"integrity": "sha512-mVRvYnTOZJz3ccpxhr3wgxVmSeiYinW+zlzQz3SXWaJmD1DuL05Jeq7nKw3SnbKmbleW5qrLG5vdyWe/A9sXhw==",
|
|
||||||
"requires": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@types/mongodb": {
|
|
||||||
"version": "3.6.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.3.tgz",
|
|
||||||
"integrity": "sha512-6YNqGP1hk5bjUFaim+QoFFuI61WjHiHE1BNeB41TA00Xd2K7zG4lcWyLLq/XtIp36uMavvS5hoAUJ+1u/GcX2Q==",
|
|
||||||
"requires": {
|
|
||||||
"@types/bson": "*",
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@types/node": {
|
|
||||||
"version": "14.14.16",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.16.tgz",
|
|
||||||
"integrity": "sha512-naXYePhweTi+BMv11TgioE2/FXU4fSl29HAH1ffxVciNsH3rYXjNP2yM8wqmSm7jS20gM8TIklKiTen+1iVncw=="
|
|
||||||
},
|
|
||||||
"angular-font-awesome": {
|
|
||||||
"version": "3.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/angular-font-awesome/-/angular-font-awesome-3.1.2.tgz",
|
|
||||||
"integrity": "sha1-k3hzJhLY6MceDXwvqg+t3H+Fjsk="
|
|
||||||
},
|
|
||||||
"bl": {
|
|
||||||
"version": "2.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz",
|
|
||||||
"integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==",
|
|
||||||
"requires": {
|
|
||||||
"readable-stream": "^2.3.5",
|
|
||||||
"safe-buffer": "^5.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"bluebird": {
|
|
||||||
"version": "3.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
|
|
||||||
"integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA=="
|
|
||||||
},
|
|
||||||
"bson": {
|
|
||||||
"version": "1.1.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz",
|
|
||||||
"integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg=="
|
|
||||||
},
|
|
||||||
"core-util-is": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
|
||||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
|
||||||
},
|
|
||||||
"debug": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
|
||||||
"requires": {
|
|
||||||
"ms": "2.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"ms": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
|
||||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"denque": {
|
|
||||||
"version": "1.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz",
|
|
||||||
"integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ=="
|
|
||||||
},
|
|
||||||
"font-awesome": {
|
|
||||||
"version": "4.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
|
|
||||||
"integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM="
|
|
||||||
},
|
|
||||||
"inherits": {
|
|
||||||
"version": "2.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
|
||||||
},
|
|
||||||
"isarray": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
|
||||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
|
||||||
},
|
|
||||||
"kareem": {
|
|
||||||
"version": "2.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz",
|
|
||||||
"integrity": "sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ=="
|
|
||||||
},
|
|
||||||
"memory-pager": {
|
|
||||||
"version": "1.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
|
|
||||||
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"mongodb": {
|
|
||||||
"version": "3.6.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.3.tgz",
|
|
||||||
"integrity": "sha512-rOZuR0QkodZiM+UbQE5kDsJykBqWi0CL4Ec2i1nrGrUI3KO11r6Fbxskqmq3JK2NH7aW4dcccBuUujAP0ERl5w==",
|
|
||||||
"requires": {
|
|
||||||
"bl": "^2.2.1",
|
|
||||||
"bson": "^1.1.4",
|
|
||||||
"denque": "^1.4.1",
|
|
||||||
"require_optional": "^1.0.1",
|
|
||||||
"safe-buffer": "^5.1.2",
|
|
||||||
"saslprep": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mongoose": {
|
|
||||||
"version": "5.11.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.11.8.tgz",
|
|
||||||
"integrity": "sha512-RRfrYLg7pyuyx7xu5hwadjIZZJB9W2jqIMkL1CkTmk/uOCX3MX2tl4BVIi2rJUtgMNwn6dy3wBD3soB8I9Nlog==",
|
|
||||||
"requires": {
|
|
||||||
"@types/mongodb": "^3.5.27",
|
|
||||||
"bson": "^1.1.4",
|
|
||||||
"kareem": "2.3.2",
|
|
||||||
"mongodb": "3.6.3",
|
|
||||||
"mongoose-legacy-pluralize": "1.0.2",
|
|
||||||
"mpath": "0.8.1",
|
|
||||||
"mquery": "3.2.3",
|
|
||||||
"ms": "2.1.2",
|
|
||||||
"regexp-clone": "1.0.0",
|
|
||||||
"safe-buffer": "5.2.1",
|
|
||||||
"sift": "7.0.1",
|
|
||||||
"sliced": "1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mongoose-legacy-pluralize": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ=="
|
|
||||||
},
|
|
||||||
"mpath": {
|
|
||||||
"version": "0.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.1.tgz",
|
|
||||||
"integrity": "sha512-norEinle9aFc05McBawVPwqgFZ7npkts9yu17ztIVLwPwO9rq0OTp89kGVTqvv5rNLMz96E5iWHpVORjI411vA=="
|
|
||||||
},
|
|
||||||
"mquery": {
|
|
||||||
"version": "3.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.3.tgz",
|
|
||||||
"integrity": "sha512-cIfbP4TyMYX+SkaQ2MntD+F2XbqaBHUYWk3j+kqdDztPWok3tgyssOZxMHMtzbV1w9DaSlvEea0Iocuro41A4g==",
|
|
||||||
"requires": {
|
|
||||||
"bluebird": "3.5.1",
|
|
||||||
"debug": "3.1.0",
|
|
||||||
"regexp-clone": "^1.0.0",
|
|
||||||
"safe-buffer": "5.1.2",
|
|
||||||
"sliced": "1.0.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"safe-buffer": {
|
|
||||||
"version": "5.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ms": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
|
||||||
},
|
|
||||||
"process-nextick-args": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
|
||||||
},
|
|
||||||
"readable-stream": {
|
|
||||||
"version": "2.3.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
|
||||||
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
|
|
||||||
"requires": {
|
|
||||||
"core-util-is": "~1.0.0",
|
|
||||||
"inherits": "~2.0.3",
|
|
||||||
"isarray": "~1.0.0",
|
|
||||||
"process-nextick-args": "~2.0.0",
|
|
||||||
"safe-buffer": "~5.1.1",
|
|
||||||
"string_decoder": "~1.1.1",
|
|
||||||
"util-deprecate": "~1.0.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"safe-buffer": {
|
|
||||||
"version": "5.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"regexp-clone": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw=="
|
|
||||||
},
|
|
||||||
"require_optional": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
|
|
||||||
"requires": {
|
|
||||||
"resolve-from": "^2.0.0",
|
|
||||||
"semver": "^5.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"resolve-from": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
|
|
||||||
"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
|
|
||||||
},
|
|
||||||
"safe-buffer": {
|
|
||||||
"version": "5.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
|
||||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
|
||||||
},
|
|
||||||
"saslprep": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
|
||||||
"sparse-bitfield": "^3.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"semver": {
|
|
||||||
"version": "5.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
|
||||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
|
||||||
},
|
|
||||||
"sift": {
|
|
||||||
"version": "7.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/sift/-/sift-7.0.1.tgz",
|
|
||||||
"integrity": "sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g=="
|
|
||||||
},
|
|
||||||
"sliced": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
|
|
||||||
"integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E="
|
|
||||||
},
|
|
||||||
"sparse-bitfield": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
|
|
||||||
"integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=",
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
|
||||||
"memory-pager": "^1.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"string_decoder": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
|
||||||
"requires": {
|
|
||||||
"safe-buffer": "~5.1.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"safe-buffer": {
|
|
||||||
"version": "5.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"util-deprecate": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
|
||||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user