Livebeat is now able to send, store and show beats

This commit is contained in:
2020-10-23 00:39:36 +02:00
parent f722ee9595
commit 13f8437f29
52 changed files with 1948 additions and 442 deletions

View File

@@ -35,13 +35,16 @@ android {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.navigation:navigation-fragment-ktx:2.2.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.2.2'
implementation 'com.squareup.moshi:moshi:1.11.0'
implementation 'com.squareup.moshi:moshi-kotlin:1.11.0'
implementation 'com.rabbitmq:amqp-client:5.9.0'
implementation "com.squareup.okhttp3:okhttp:4.9.0"
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

View File

@@ -1,9 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="de.nicolasklier.livebeat">
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"></uses-permission>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-feature
android:name="android.hardware.sensor.gyroscope"
android:required="true" />
<application
android:allowBackup="true"
@@ -11,6 +18,7 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.Livebeat">
<service android:name=".TrackerService" />
<activity

View File

@@ -1,31 +0,0 @@
package de.nicolasklier.livebeat
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.navigation.fragment.findNavController
/**
* A simple [Fragment] subclass as the default destination in the navigation.
*/
class FirstFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_first, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<Button>(R.id.button_first).setOnClickListener {
findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
}
}
}

View File

@@ -1,24 +1,202 @@
package de.nicolasklier.livebeat
import android.Manifest
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.util.logging.Logger
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
class MainActivity : AppCompatActivity() {
companion object {
@JvmStatic val API_URL = "http://192.168.178.26:8040"
var TOKEN = ""
}
private var broadcastReceiver: BroadcastReceiver? = null
@SuppressLint("HardwareIds")
fun checkIfPhoneIsRegistered() {
if (TOKEN == "") return;
Thread(Runnable {
val androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
val client = OkHttpClient()
val req = Request.Builder()
.url("$API_URL/phone/$androidId")
.get()
.build()
val response = client.newCall(req).execute()
if (response.code != 200) {
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Device isn't registered yet.", Snackbar.LENGTH_SHORT)
.setBackgroundTint(Color.YELLOW)
.setTextColor(Color.BLACK)
.show()
// Register device
val phone = Phone(
androidId,
Build.MODEL,
Build.PRODUCT,
Build.VERSION.RELEASE,
System.getProperty("os.arch")
)
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val phoneToJson = moshi.adapter(Phone::class.java)
val json = phoneToJson.toJson(phone)
val createPhone = Request.Builder()
.url("$API_URL/phone")
.post(
(json).toRequestBody()
)
.header("Content-Type", "application/json")
.header("token", TOKEN)
.build()
client.newCall(createPhone).execute()
}
}).start()
}
override fun onCreate(savedInstanceState: Bundle?) {
checkPerms()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
startService(Intent(this, TrackerService::class.java))
// Check authorization
val backendChecks = Thread(Runnable {
val username = findViewById<TextView>(R.id.username).text
val password = findViewById<TextView>(R.id.password).text
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val jsonToLogin = moshi.adapter(Login::class.java)
val client = OkHttpClient()
val req = Request.Builder()
.url("$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
// Since we are in another thread, we have to get back to the UI thread.
this.runOnUiThread(Runnable {
findViewById<TextView>(R.id.httpStatus).setTextColor(Color.GREEN)
findViewById<TextView>(R.id.httpStatus).text = "CONNECTED"
})
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Login succeeded", Snackbar.LENGTH_SHORT)
.setBackgroundTint(Color.GREEN)
.setActionTextColor(Color.WHITE)
.show()
checkIfPhoneIsRegistered()
} else {
// Since we are in another thread, we have to get back to the UI thread.
this.runOnUiThread(Runnable {
findViewById<TextView>(R.id.httpStatus).setTextColor(Color.RED)
findViewById<TextView>(R.id.httpStatus).text = "LOGIN FAILED"
})
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Login failed", Snackbar.LENGTH_LONG)
.setBackgroundTint(Color.RED)
.setActionTextColor(Color.WHITE)
.show()
}
})
backendChecks.start()
/**
* Here we receive updates from our tracker service.
*/
this.broadcastReceiver = object : BroadcastReceiver() {
@SuppressLint("CutPasteId")
override fun onReceive(context: Context, intent: Intent) {
val statusRabbit = intent.getBooleanExtra("statusRabbit", false)
val statusHttp = intent.getIntExtra("statusHttp", 404)
if (statusRabbit) {
findViewById<TextView>(R.id.rabbitStatus).text = "connected"
findViewById<TextView>(R.id.rabbitStatus).setTextColor(Color.GREEN)
} else {
findViewById<TextView>(R.id.rabbitStatus).text = "disconnected"
findViewById<TextView>(R.id.rabbitStatus).setTextColor(Color.RED)
}
if (statusHttp == 200) {
findViewById<TextView>(R.id.httpStatus).text = "ONLINE (no login)"
findViewById<TextView>(R.id.httpStatus).setTextColor(Color.CYAN)
findViewById<Button>(R.id.signin).isEnabled = false
findViewById<Button>(R.id.signin).text = "Signed in"
} else {
findViewById<TextView>(R.id.httpStatus).text = "OFFLINE"
findViewById<TextView>(R.id.httpStatus).setTextColor(Color.RED)
findViewById<Button>(R.id.signin).isEnabled = true
findViewById<Button>(R.id.signin).text = "Sign in"
}
}
}
registerReceiver(broadcastReceiver, IntentFilter("de.nicolasklier.livebeat"))
findViewById<FloatingActionButton>(R.id.fab).setOnClickListener { view ->
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show()
.setAction("Action", null).show()
}
}
private fun checkPerms() {
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_COARSE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 1)
}
}
override fun onStop() {
super.onStop()
if (broadcastReceiver != null) {
unregisterReceiver(broadcastReceiver)
}
}
@@ -37,4 +215,28 @@ class MainActivity : AppCompatActivity() {
else -> super.onOptionsItemSelected(item)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == 1) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Location access is allowed", Snackbar.LENGTH_SHORT)
.setBackgroundTint(Color.GREEN)
.setAction("Action", null).show()
}
} else {
Snackbar.make(findViewById<FloatingActionButton>(R.id.fab), "Location access is required.", Snackbar.LENGTH_INDEFINITE)
.setBackgroundTint(Color.RED)
.setActionTextColor(Color.WHITE)
.setAction("Grant", View.OnClickListener {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 1)
}).show()
}
}
}
}

View File

@@ -0,0 +1,26 @@
package de.nicolasklier.livebeat
import com.squareup.moshi.JsonClass
import com.squareup.moshi.ToJson
/**
* This class represents one singe beat that will be send to the database.
*/
class Beat(
final val token: String, // Token of this phone
final val gpsLocation: Array<Double>,
final val battery: Int?,
final val timestamp: Long
) {}
class Phone(
final val androidId: String,
final val modelName: String,
final val displayName: String,
final val operatingSystem: String,
final val architecture: String
) {}
class Login(
final val token: String
) {}

View File

@@ -1,31 +0,0 @@
package de.nicolasklier.livebeat
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.navigation.fragment.findNavController
/**
* A simple [Fragment] subclass as the second destination in the navigation.
*/
class SecondFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_second, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<Button>(R.id.button_second).setOnClickListener {
findNavController().navigate(R.id.action_SecondFragment_to_FirstFragment)
}
}
}

View File

@@ -1,96 +0,0 @@
package de.nicolasklier.livebeat;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Color;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class TrackerService extends Service {
private static final int NOTIF_ID = 1;
private static final String NOTIF_CHANNEL_ID = "LiveTracker";
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
final Connection[] conn = new Connection[1];
// This thread only connects to RabbitMQ
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("lineage");
factory.setPassword("ZSo$X97GQ547JXL7nGq");
factory.setVirtualHost("/");
factory.setHost("192.168.178.26");
factory.setPort(5672);
try {
conn[0] = factory.newConnection();
Channel channel = conn[0].createChannel();
channel.queueDeclare("Tracker", false, false, false, null);
channel.basicPublish("", "Tracker", null, "Test message!!!!!!!!!!!!!!!!!".getBytes());
Log.i("RabbitMQ", "run: Published test message");
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
});
thread.start();
startForeground();
return super.onStartCommand(intent, flags, startId);
}
private void startForeground() {
Intent noticicationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, noticicationIntent, 0);
NotificationChannel chan = new NotificationChannel(
NOTIF_CHANNEL_ID,
"Tracker",
NotificationManager.IMPORTANCE_LOW);
chan.setLightColor(Color.BLACK);
chan.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
assert manager != null;
manager.createNotificationChannel(chan);
startForeground(NOTIF_ID, new NotificationCompat.Builder(this, NOTIF_CHANNEL_ID)
.setOngoing(true)
.setContentTitle("Livebeat")
.setContentText("Tracker is running")
.setContentIntent(pendingIntent)
.setCategory(Notification.CATEGORY_SERVICE)
.setPriority(NotificationManager.IMPORTANCE_LOW)
.setChannelId(NOTIF_CHANNEL_ID)
.setColor(Color.BLACK)
.build());
}
}

View File

@@ -0,0 +1,143 @@
package de.nicolasklier.livebeat
import android.Manifest
import android.annotation.SuppressLint
import android.app.*
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.graphics.Color
import android.location.LocationManager
import android.os.BatteryManager
import android.os.Bundle
import android.os.IBinder
import android.provider.Settings
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import com.rabbitmq.client.Channel
import com.rabbitmq.client.Connection
import com.rabbitmq.client.ConnectionFactory
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.IOException
import java.util.concurrent.TimeoutException
class TrackerService : Service() {
val conn = arrayOfNulls<Connection>(1)
val channel = arrayOfNulls<Channel>(1)
override fun onBind(intent: Intent): IBinder? {
return null
}
private fun connectWithBroker() {
// This thread only connects to RabbitMQ
val connectionThread = Thread(Runnable {
val client = OkHttpClient()
val req = Request.Builder()
.url(MainActivity.API_URL)
.get()
.build()
val factory = ConnectionFactory()
factory.username = "lineage"
factory.password = "ZSo\$X97GQ547JXL7nGq"
factory.virtualHost = "/"
factory.host = "nk-home.ddns.net"
factory.port = 5672
factory.isAutomaticRecoveryEnabled = true
try {
conn[0] = factory.newConnection()
channel[0] = conn[0]?.createChannel()
val intent = Intent("de.nicolasklier.livebeat")
val bundle = Bundle()
bundle.putBoolean("statusRabbit", true)
bundle.putInt("statusHttp", client.newCall(req).execute().code)
intent.putExtras(bundle)
this.sendBroadcast(intent)
channel[0]?.queueDeclare("tracker", true, false, false, null)
//channel[0]?.basicPublish("", "Tracker", null, "Test message".toByteArray())
Log.i("RabbitMQ", "run: Published test message")
} catch (e: IOException) {
e.printStackTrace()
} catch (e: TimeoutException) {
e.printStackTrace()
}
})
connectionThread.start()
}
@SuppressLint("HardwareIds")
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
// Android id as unique identifier
val androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
locationManager.requestLocationUpdates("gps", 5000, 0f
) { location ->
Log.i("Location", "Location is: " + location.latitude + " | " + location.longitude)
/* Get battery */
val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { ifilter ->
this@TrackerService.registerReceiver(null, ifilter)
}
val batteryPct: Float? = batteryStatus?.let { intent ->
val level: Int = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val scale: Int = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
level * 100 / scale.toFloat()
}
val beat = Beat(androidId, arrayOf(location.latitude, location.longitude, location.accuracy.toDouble(), location.speed.toDouble()), batteryPct?.toInt(), location.time)
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val jsonAdapter: JsonAdapter<Beat> = moshi.adapter(Beat::class.java)
Thread(Runnable {
channel[0]?.basicPublish("", "tracker", null, jsonAdapter.toJson(beat).toByteArray())
}).start()
}
}
connectWithBroker()
startForeground()
return super.onStartCommand(intent, flags, startId)
}
private fun startForeground() {
val noticicationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this, 0, noticicationIntent, 0)
val chan = NotificationChannel(
NOTIF_CHANNEL_ID,
"Tracker",
NotificationManager.IMPORTANCE_LOW)
chan.lightColor = Color.BLACK
chan.lockscreenVisibility = Notification.VISIBILITY_SECRET
val manager = (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
manager.createNotificationChannel(chan)
startForeground(NOTIF_ID, NotificationCompat.Builder(this, NOTIF_CHANNEL_ID)
.setOngoing(true)
.setContentTitle("Livebeat")
.setContentText("Tracker is running")
.setContentIntent(pendingIntent)
.setCategory(Notification.CATEGORY_SERVICE)
.setPriority(NotificationManager.IMPORTANCE_LOW)
.setChannelId(NOTIF_CHANNEL_ID)
.setColorized(true)
.setColor(Color.BLACK)
.build())
}
companion object {
private const val NOTIF_ID = 1
private const val NOTIF_CHANNEL_ID = "LiveTracker"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

View File

@@ -15,12 +15,127 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:background="@color/black"
app:popupTheme="@style/Theme.Livebeat.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_main" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black2"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<Space
android:layout_width="match_parent"
android:layout_height="50dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:id="@+id/httpText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center|center_horizontal|center_vertical"
android:text="Backend"
android:textAlignment="center"
android:textColor="@color/white" />
<TextView
android:id="@+id/httpStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:layout_weight="0"
android:fontFamily="sans-serif-black"
android:text="disconnected"
android:textAlignment="viewStart"
android:textAllCaps="true"
android:textColor="@color/orange"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<TextView
android:id="@+id/rabbitText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|center_vertical"
android:layout_weight="0"
android:text="RabbitMQ"
android:textColor="@color/white" />
<TextView
android:id="@+id/rabbitStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:layout_weight="0"
android:fontFamily="sans-serif-black"
android:text="disconnected"
android:textAlignment="center"
android:textAllCaps="true"
android:textColor="@color/orange"
android:textSize="14sp" />
</LinearLayout>
<Space
android:layout_width="match_parent"
android:layout_height="120dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="80dp"
android:layout_marginRight="80dp"
android:orientation="vertical">
<EditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@color/white"
android:ems="10"
android:inputType="textPersonName"
android:text="admin"
android:textColor="@color/white" />
<EditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@color/white"
android:ems="10"
android:hint="Secret password"
android:inputType="textPassword"
android:text="$1KDaNCDlyXAOg"
android:textColor="@color/white"
android:textColorHint="@color/white" />
<Button
android:id="@+id/signin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Sign in" />
</LinearLayout>
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
@@ -28,6 +143,8 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
app:srcCompat="@android:drawable/ic_dialog_email" />
app:backgroundTint="@color/orange"
app:rippleColor="#FFFFFF"
app:srcCompat="@drawable/round_clear_white_18dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -1,19 +0,0 @@
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,28 +0,0 @@
<?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=".FirstFragment">
<TextView
android:id="@+id/textview_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_first_fragment"
app:layout_constraintBottom_toTopOf="@id/button_first"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/next"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textview_first" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,27 +0,0 @@
<?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=".SecondFragment">
<TextView
android:id="@+id/textview_second"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/button_second"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button_second"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/previous"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textview_second" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

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

View File

@@ -1,16 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Livebeat" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -1,10 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="black">#0D0D0D</color>
<color name="black2">#1C1C1C</color>
<color name="green">#B4D9C4</color>
<color name="orange">#F24B0F</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -1,16 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Livebeat" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<style name="Theme.Livebeat" parent="Theme.AppCompat.Light">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorPrimary">@color/black</item>
<item name="colorPrimaryVariant">@color/black2</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<item name="colorSecondary">@color/orange</item>
<item name="colorSecondaryVariant">@color/orange</item>
<item name="colorOnSecondary">@color/white</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<item name="android:statusBarColor">@color/black2</item>
<!-- Customize your theme here. -->
</style>
@@ -20,6 +20,5 @@
</style>
<style name="Theme.Livebeat.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="Theme.Livebeat.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>

View File

@@ -4,6 +4,7 @@ import { config as dconfig } from 'dotenv';
import * as express from 'express';
import * as figlet from 'figlet';
import * as mongoose from 'mongoose';
import * as cors from 'cors';
import { exit } from 'process';
import * as winston from 'winston';
import { RabbitMQ } from './lib/rabbit';
@@ -13,6 +14,8 @@ import { DeleteUser, GetUser, LoginUser, MW_User, PatchUser } from './endpoints/
import { hashPassword, randomPepper, randomString } from './lib/crypto';
import { UserType } from './models/user/user.interface';
import { User } from './models/user/user.model';
import { GetPhone, PostPhone } from './endpoints/phone';
import { GetBeat } from './endpoints/beat';
// Load .env
dconfig({ debug: true, encoding: 'UTF-8' });
@@ -127,15 +130,29 @@ async function run() {
*/
const app = express();
app.use(express.json());
app.use(cors());
app.use(bodyParser.json({ limit: '5kb' }));
app.use((req, res, next) => {
res.on('finish', () => {
const done = Date.now();
logger.debug(`${req.method} - ${req.url} ${JSON.stringify(req.body)} -> ${res.statusCode}`);
});
next();
});
app.get('/', (req, res) => res.status(200).send('OK'));
app.get('/user/', MW_User, (req, res) => GetUser(req, res));
app.get('/user/:id', MW_User, (req, res) => GetUser(req, res));
app.patch('/user/:id', MW_User, (req, res) => PatchUser(req, res));
app.delete('/user/:id', MW_User, (req, res) => DeleteUser(req, res));
app.post('/user/login', (req, res) => LoginUser(req, res));
app.get('/phone/:id', (req, res) => GetPhone(req, res));
app.post('/phone', MW_User, (req, res) => PostPhone(req, res));
app.get('/beat/', MW_User, (req, res) => GetBeat(req, res));
app.listen(config.http.port, config.http.host, () => {
logger.info(`HTTP server is running at ${config.http.host}:${config.http.port}`);
});

30
backend/endpoints/beat.ts Normal file
View File

@@ -0,0 +1,30 @@
import { Response } from "express";
import { logger } from "../app";
import { LivebeatRequest } from "../lib/request";
import { Beat } from "../models/beat/beat.model.";
import { Phone } from "../models/phone/phone.model";
export interface IFilter {
phone: string,
time: {
from: number,
to: number
},
max: number
}
export async function GetBeat(req: LivebeatRequest, res: Response) {
const filter: IFilter = req.body.filter as IFilter;
// If no filters are specified, we return the last 500 points. We take the first phone as default.
if (filter === undefined) {
const phone = await Phone.findOne({ user: req.user?._id });
logger.debug(`No filters were provided! Take ${phone?.displayName} as default.`);
if (phone !== undefined && phone !== null) {
logger.debug("Query for latest beats ...");
const beats = await Beat.find({ phone: phone._id }).limit(800).sort({ _id: -1 });
res.status(200).send(beats);
}
}
}

View File

@@ -0,0 +1,63 @@
import { Response } from "express";
import { logger } from "../app";
import { LivebeatRequest } from "../lib/request";
import { Phone } from "../models/phone/phone.model";
export async function GetPhone(req: LivebeatRequest, res: Response) {
const phoneId: String = req.params['id'];
if (phoneId === undefined) {
res.status(400).send();
return;
}
// Check database for phone
const phone = await Phone.findOne({ androidId: phoneId, user: req.user?._id });
if (phone === undefined) {
res.status(404).send();
return;
}
res.status(200).send(phone);
}
export async function PostPhone(req: LivebeatRequest, res: Response) {
const androidId: String = req.body.androidId;
const modelName: String = req.body.modelName;
const displayName: String = req.body.displayName;
const operatingSystem: String = req.body.operatingSystem;
const architecture: String = req.body.architecture;
if (androidId === undefined ||
modelName === undefined ||
displayName === undefined ||
operatingSystem === undefined ||
architecture === undefined) {
logger.debug("Request to /phone failed because of missing parameters.");
res.status(400).send();
return;
}
// Check if phone already exists
const phone = await Phone.findOne({ androidId, user: req.user?._id });
if (phone !== null) {
logger.debug("Request to /phone failed because phone already exists.");
res.status(409).send();
return;
}
// Create phone
await Phone.create({
androidId,
displayName,
modelName,
operatingSystem,
architecture,
user: req.user?._id
});
logger.info(`New device (${displayName}) registered for ${req.user?.name}.`)
res.status(200).send();
}

View File

@@ -3,7 +3,8 @@ import { verifyPassword } from "../lib/crypto";
import { User } from "../models/user/user.model";
import { sign, decode, verify } from 'jsonwebtoken';
import { JWT_SECRET, logger } from "../app";
import { IUser } from "../models/user/user.interface";
import { LivebeatRequest } from '../lib/request';
import { SchemaTypes } from "mongoose";
export async function GetUser(req: Request, res: Response) {
@@ -48,7 +49,7 @@ export async function LoginUser(req: Request, res: Response) {
}
// We're good. Create JWT token.
const token = sign({ user: user._id }, JWT_SECRET!, { notBefore: Date.now(), expiresIn: '30d' });
const token = sign({ user: user._id }, JWT_SECRET, { expiresIn: '30d' });
logger.info(`User ${user.name} logged in.`)
res.status(200).send({ token });
@@ -58,29 +59,34 @@ export async function LoginUser(req: Request, res: Response) {
* This middleware validates any tokens that are required to access most of the endpoints.
* Note: This validation doesn't contain any permission checking.
*/
export async function MW_User(req: Request, res: Response, next: () => void) {
export async function MW_User(req: LivebeatRequest, res: Response, next: () => void) {
if (req.headers.token === undefined) {
res.status(401).send();
res.status(401).send({ message: "Token not specified" });
return;
}
const token = req.headers.token.toString();
try {
// Verify token
if(await verify(token, JWT_SECRET!, { algorithms: ['HS256'] })) {
if(await verify(token, JWT_SECRET, { algorithms: ['HS256'] })) {
// Token is valid, now look if user is in db (in case he got deleted)
const id: number = Number(decode(token, { json: true })!.id);
const db = await User.findOne({ where: { id } });
if (db !== undefined) {
const id = decode(token, { json: true })!.user;
const db = await User.findById(id);
if (db !== undefined && db !== null) {
req.user = db
next();
return;
} else {
res.status(401).send();
res.status(401).send({ message: "Token is not valid" });
}
} else {
res.status(401).send();
res.status(401).send({ message: "Token is not valid" });
}
} catch (err) {
if (err) res.status(401).send();
if (err) {
res.status(500).send({ message: "We failed validating your token for some reason." });
logger.error(err);
}
}
}

View File

@@ -1,5 +1,14 @@
import * as amqp from 'amqplib';
import { logger, RABBITMQ_URI } from '../app';
import { Beat } from '../models/beat/beat.model.';
import { Phone } from '../models/phone/phone.model';
interface IBeat {
token: string,
gpsLocation: Array<number>,
battery: number,
timestamp: number
}
export class RabbitMQ {
connection: amqp.Connection | null = null;
@@ -9,12 +18,32 @@ export class RabbitMQ {
this.connection = await amqp.connect(RABBITMQ_URI);
this.channel = await this.connection.createChannel();
this.channel.consume('Tracker', (msg) => {
logger.debug("Received from broker: " + msg?.content.toString());
}, { noAck: false });
this.channel.consume('tracker', async (income) => {
if (income === undefined || income === null) return;
const msg: IBeat = JSON.parse(income.content.toString()) as IBeat
// Get phone
const phone = await Phone.findOne({ androidId: msg.token });
if (phone == undefined) {
logger.info(`Received beat from unknown device with id ${msg.token}`);
return;
}
logger.info(`New beat from ${phone.displayName} with ${msg.gpsLocation[2]} accuracy and ${msg.battery}% battery`)
Beat.create({
phone: phone._id,
coordinate: [msg.gpsLocation[0], msg.gpsLocation[1]],
accuracy: msg.gpsLocation[2],
speed: msg.gpsLocation[3],
battery: msg.battery,
createdAt: msg.timestamp
});
}, { noAck: true });
}
async publish(queueName = 'Tracker', data: any) {
async publish(queueName = 'tracker', data: any) {
if (this.connection == undefined) await this.init()
this.channel?.sendToQueue(queueName, Buffer.from(data));
}

6
backend/lib/request.ts Normal file
View File

@@ -0,0 +1,6 @@
import { Request } from "express";
import { IUser } from "../models/user/user.interface";
export interface LivebeatRequest extends Request {
user?: IUser
}

View File

@@ -1,11 +1,11 @@
import { Document } from 'mongoose';
import { IPhone } from '../phone/phone.interface';
export interface ITrack extends Document {
export interface IBeat extends Document {
coordinate?: number[],
velocity?: number,
accuracy: number,
speed: number,
battery?: number,
magneticField?: number,
phone: IPhone,
createdAt?: Date
}

View File

@@ -0,0 +1,6 @@
import { Model, model } from 'mongoose';
import { IBeat } from './beat.interface';
import { schemaBeat } from './beat.schema';
const modelBeat: Model<IBeat> = model<IBeat>('Beat', schemaBeat, 'Beat');
export { modelBeat as Beat };

View File

@@ -0,0 +1,16 @@
import { Schema, SchemaTypes } from 'mongoose';
import { Phone } from '../phone/phone.model';
const schemaBeat = new Schema({
coordinate: { type: [Number], required: false },
accuracy: { type: Number, required: false },
speed: { type: Number, required: false },
battery: { type: Number, required: false },
phone: { type: SchemaTypes.ObjectId, required: true, default: 'user' }
}, {
timestamps: {
createdAt: true
}
});
export { schemaBeat };

View File

@@ -2,6 +2,7 @@ import { Document } from 'mongoose';
import { IUser } from '../user/user.interface';
export interface IPhone extends Document {
androidId: String,
displayName: String,
modelName: String,
operatingSystem: String,

View File

@@ -1,12 +1,13 @@
import { Schema } from 'mongoose';
import { Mongoose, Schema, SchemaType, SchemaTypes } from 'mongoose';
import { User } from '../user/user.model';
const schemaPhone = new Schema({
androidId: { type: String, required: true },
displayName: { type: String, required: true },
modelName: { type: String, required: false },
operatingSystem: { type: String, required: false },
architecture: { type: String, required: false },
user: { type: User, required: true }
user: { type: SchemaTypes.ObjectId, required: true }
}, {
timestamps: {
createdAt: true,

View File

@@ -1,6 +0,0 @@
import { Model, model } from 'mongoose';
import { ITrack } from './track.interface';
import { schemaTrack } from './track.schema';
const modelTrack: Model<ITrack> = model<ITrack>('Track', schemaTrack, 'Track');
export { modelTrack as Phone };

View File

@@ -1,16 +0,0 @@
import { Schema } from 'mongoose';
import { Phone } from '../phone/phone.model';
const schemaTrack = new Schema({
coordinate: { type: [Number], required: false },
velocity: { type: Number, required: false },
battery: { type: Number, required: false },
magneticField: { type: Number, required: false },
phone: { type: Phone, required: true, default: 'user' }
}, {
timestamps: {
createdAt: true
}
});
export { schemaTrack };

View File

@@ -96,6 +96,15 @@
"@types/node": "*"
}
},
"@types/cors": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.8.tgz",
"integrity": "sha512-fO3gf3DxU2Trcbr75O7obVndW/X5k8rJNZkLXlQWStTHhP71PkRqjwPIEI0yMnJdg9R9OasjU+Bsr+Hr1xy/0w==",
"dev": true,
"requires": {
"@types/express": "*"
}
},
"@types/dotenv": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-8.2.0.tgz",
@@ -868,6 +877,15 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"requires": {
"object-assign": "^4",
"vary": "^1"
}
},
"crypto-random-string": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",

View File

@@ -21,6 +21,7 @@
"argon2": "^0.27.0",
"body-parser": "^1.19.0",
"chalk": "^4.1.0",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"figlet": "^1.5.0",
@@ -43,6 +44,7 @@
"concurrently": "^5.3.0",
"nodemon": "^2.0.5",
"@types/jsonwebtoken": "8.5.0",
"@types/amqplib": "0.5.14"
"@types/amqplib": "0.5.14",
"@types/cors": "2.8.8"
}
}

View File

@@ -28,7 +28,9 @@
"src/assets"
],
"styles": [
"src/styles.scss"
"src/styles.scss",
"./node_modules/mapbox-gl/dist/mapbox-gl.css",
"./node_modules/@mapbox/mapbox-gl-geocoder/lib/mapbox-gl-geocoder.css"
],
"scripts": []
},

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,11 @@
"@nebular/auth": "^6.2.1",
"@nebular/eva-icons": "^6.2.1",
"@nebular/theme": "^6.2.1",
"@types/mapbox-gl": "^1.12.5",
"eva-icons": "^1.1.3",
"geojson": "^0.5.0",
"mapbox-gl": "^1.12.0",
"ngx-mapbox-gl": "^4.8.1",
"rxjs": "~6.6.0",
"tslib": "^2.0.0",
"zone.js": "~0.10.2"

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { APIService } from './api.service';
describe('APIService', () => {
let service: APIService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(APIService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,62 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
export interface ILogin {
token: string;
}
export interface IBeat {
coordinate?: number[];
accuracy: number;
speed: number;
battery?: number;
phone: any;
createdAt?: Date;
}
@Injectable({
providedIn: 'root'
})
export class APIService {
private token: string;
username: string;
API_ENDPOINT = 'http://192.168.178.26:8040'
constructor(private httpClient: HttpClient) { }
async login(username: string, password: string): Promise<ILogin> {
return new Promise<ILogin>((resolve, reject) => {
console.log('POST');
this.httpClient.post(this.API_ENDPOINT + '/user/login', { username, password }, { responseType: 'json' })
.subscribe(token => {
console.log(token);
this.token = (token as ILogin).token;
this.username = username;
resolve(token as ILogin);
});
});
}
async getBeats(): Promise<IBeat[]> {
return new Promise<IBeat[]>((resolve, reject) => {
if (this.token === undefined) { reject([]); }
const headers = new HttpHeaders({ token: this.token });
this.httpClient.get(this.API_ENDPOINT + '/beat', { responseType: 'json', headers })
.subscribe(beats => {
console.log(beats);
resolve(beats as IBeat[]);
});
});
}
hasSession(): boolean {
return this.token !== undefined;
}
}

View File

@@ -1,17 +1,22 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { NbAuthComponent, NbLoginComponent } from '@nebular/auth';
import { AppComponent } from './app.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { LoginComponent } from './login/login.component';
const routes: Routes = [
{
path: '',
component: NbAuthComponent,
children: [
{
path: '',
component: NbLoginComponent
}
]
component: AppComponent
},
{
path: 'login',
component: LoginComponent
},
{
path: 'dashboard',
component: DashboardComponent
}
];

View File

@@ -1,3 +1,12 @@
<nb-auth-block>
<nb-login></nb-login>
</nb-auth-block>
<div id="header">
<p>Header</p>
<div class="left">
<span class="ident" *ngIf="this.api.hasSession()">Logged in as {{this.api.username}}</span>
</div>
</div>
<router-outlet></router-outlet>
<!-- Display start page -->
<div id="startpage" *ngIf="false">
<h1>Livebeat</h1>
</div>

View File

@@ -0,0 +1,10 @@
#header {
width: 100vw;
height: fit-content;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
background-color: #1d1d1dd9;
backdrop-filter: blur(20px);
box-shadow: 10px 10px 50px 0px rgba(0,0,0,0.85);
}

View File

@@ -1,4 +1,6 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { APIService } from './api.service';
@Component({
selector: 'app-root',
@@ -7,4 +9,7 @@ import { Component } from '@angular/core';
})
export class AppComponent {
title = 'Livebeat';
constructor(public api: APIService, private router: Router) {
}
}

View File

@@ -8,16 +8,26 @@ import { NbCardModule, NbLayoutModule, NbSidebarModule, NbThemeModule } from '@n
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { FormsModule } from '@angular/forms';
import { DashboardComponent } from './dashboard/dashboard.component';
import { NgxMapboxGLModule } from 'ngx-mapbox-gl';
@NgModule({
declarations: [
AppComponent
AppComponent,
LoginComponent,
DashboardComponent
],
imports: [
BrowserModule,
AppRoutingModule,
BrowserAnimationsModule,
FormsModule,
HttpClientModule,
NgxMapboxGLModule.withConfig({
accessToken: 'pk.eyJ1IjoibW9uZGVpMSIsImEiOiJja2dsY2ZtaG0xZ2o5MnR0ZWs0Mm82OTBpIn0.NzDWN3P6jJLmci_v3MM1tA'
}),
NbThemeModule.forRoot({ name: 'dark' }),
NbLayoutModule,
NbEvaIconsModule,

View File

@@ -0,0 +1,13 @@
<mgl-map [style]="'mapbox://styles/mapbox/outdoors-v11'">
<mgl-geojson-source id="locHistory" [data]="data"></mgl-geojson-source>
<mgl-layer
id="locHisotryLines"
type="line"
source="locHistory"
[paint]="{
'line-color': '#ff0000',
'line-width': 4
}"
>
</mgl-layer>
</mgl-map>

View File

@@ -0,0 +1,8 @@
mgl-map {
position: absolute;
z-index: -1;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DashboardComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,32 @@
import { AfterViewInit, Component, OnInit } from '@angular/core';
import { Map } from 'mapbox-gl';
import { APIService } from '../api.service';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss']
})
export class DashboardComponent implements AfterViewInit {
map: Map;
data: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
type: 'FeatureCollection', features: [
{
type: 'Feature',
properties: null,
geometry: { type: 'LineString', coordinates: [] }
}]
};
constructor(private api: APIService) { }
async ngAfterViewInit(): Promise<void> {
const beats = await this.api.getBeats();
beats.forEach((beat) => {
this.data.features[0].geometry.coordinates.push([beat.coordinate[1], beat.coordinate[0]]);
});
console.log("Now:", this.data.features);
}
}

View File

@@ -0,0 +1,16 @@
<div id="login">
<h2>Login</h2>
<p>Please authenticate yourself to proceed.</p>
<form>
<div id="username">
<label>Username</label><br>
<input type="text" name="username" placeholder="Username" [(ngModel)]="username">
</div>
<div id="password">
<label>Password</label><br>
<input type="password" name="password" placeholder="Password" [(ngModel)]="password">
</div>
<button (click)="perfomLogin()">Login</button>
</form>
</div>

View File

@@ -0,0 +1,13 @@
#login {
position: relative;
margin: 0 auto;
display: block;
height: fit-content;
width: fit-content;
padding: 5rem;
border-radius: 20px;
background: #1d1d1d;
box-shadow: 20px 20px 60px #191919,
-20px -20px 60px #212121;
}

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginComponent } from './login.component';
describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ LoginComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,31 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { APIService } from '../api.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
username = '';
password = '';
constructor(private api: APIService, private router: Router) { }
ngOnInit(): void {
}
async perfomLogin(): Promise<any> {
console.log('Clicked!');
if ((await this.api.login(this.username, this.password)).token !== undefined) {
console.log('Login was successful!');
this.router.navigate(['dashboard']);
} else {
console.log('Login was not successful!');
}
}
}

View File

@@ -61,3 +61,4 @@ import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
(window as any).global = window;

View File

@@ -2,9 +2,18 @@
@import '~@nebular/auth/styles/globals';
@import '~@nebular/theme/styles/globals';
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;700;900&display=swap');
@include nb-install() {
@include nb-theme-global();
@include nb-auth-global();
@include nb-theme-global();
};
body {
background-color: #1d1d1d;
color: #fff;
font-family: 'Inter', sans-serif;
margin: 0;
padding: 0;
}
/* You can add global styles to this file, and also import other style files */