From d08095ece7948e6997898a9027a1b3fc9ba08ab1 Mon Sep 17 00:00:00 2001 From: Youssef Raafat Date: Thu, 17 Oct 2019 06:53:48 +0200 Subject: [PATCH] Authenticate Staff Accounts * Create Auth class to enclose authentication functions. * Create App class to get app context from object classes. * Create Accounts models. * Create SplashScreen to run while async tasks are running on startup. * Add sign out button to Main Activity. * Use Kotlin Coroutines to create async tasks. * Update and sort string resources. * Replace "Bus Id" with "Id". * Rename SharedPrefHelper to SharedPref. * Rename LogInActivity to SignInActivity. --- app/build.gradle | 5 +- app/src/main/AndroidManifest.xml | 13 ++- app/src/main/java/com/tazkrtak/staff/App.kt | 17 ++++ .../staff/activities/LogInActivity.kt | 67 --------------- .../tazkrtak/staff/activities/MainActivity.kt | 35 +++----- .../staff/activities/SignInActivity.kt | 66 ++++++++++++++ .../staff/activities/SplashScreenActivity.kt | 49 +++++++++++ .../java/com/tazkrtak/staff/models/Account.kt | 21 +++++ .../java/com/tazkrtak/staff/models/Bus.kt | 1 + .../com/tazkrtak/staff/models/Collector.kt | 9 ++ .../com/tazkrtak/staff/models/Conductor.kt | 10 +++ .../main/java/com/tazkrtak/staff/util/Auth.kt | 86 +++++++++++++++++++ .../main/java/com/tazkrtak/staff/util/Hash.kt | 4 +- .../com/tazkrtak/staff/util/SharedPref.kt | 25 ++++++ .../tazkrtak/staff/util/SharedPrefHelper.kt | 35 -------- app/src/main/res/layout/activity_main.xml | 10 +++ ...tivity_log_in.xml => activity_sign_in.xml} | 18 ++-- .../res/layout/activity_splash_screen.xml | 12 +++ app/src/main/res/values-ar/strings.xml | 20 ++--- app/src/main/res/values/strings.xml | 26 +++--- build.gradle | 2 +- 21 files changed, 357 insertions(+), 174 deletions(-) create mode 100644 app/src/main/java/com/tazkrtak/staff/App.kt delete mode 100644 app/src/main/java/com/tazkrtak/staff/activities/LogInActivity.kt create mode 100644 app/src/main/java/com/tazkrtak/staff/activities/SignInActivity.kt create mode 100644 app/src/main/java/com/tazkrtak/staff/activities/SplashScreenActivity.kt create mode 100644 app/src/main/java/com/tazkrtak/staff/models/Account.kt create mode 100644 app/src/main/java/com/tazkrtak/staff/models/Collector.kt create mode 100644 app/src/main/java/com/tazkrtak/staff/models/Conductor.kt create mode 100644 app/src/main/java/com/tazkrtak/staff/util/Auth.kt create mode 100644 app/src/main/java/com/tazkrtak/staff/util/SharedPref.kt delete mode 100644 app/src/main/java/com/tazkrtak/staff/util/SharedPrefHelper.kt rename app/src/main/res/layout/{activity_log_in.xml => activity_sign_in.xml} (84%) create mode 100644 app/src/main/res/layout/activity_splash_screen.xml diff --git a/app/build.gradle b/app/build.gradle index 9e57279..d65be43 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,11 +25,14 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.1.1' implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.core:core-ktx:1.1.0' implementation 'com.android.support:multidex:1.0.3' implementation 'com.google.android.material:material:1.0.0' - implementation 'com.google.firebase:firebase-firestore:21.1.1' + implementation 'com.google.firebase:firebase-common-ktx:19.2.0' + implementation 'com.google.firebase:firebase-firestore-ktx:21.2.0' implementation 'com.journeyapps:zxing-android-embedded:4.0.0' implementation 'com.google.zxing:core:3.4.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 933304a..3e3e2fa 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,9 +3,10 @@ xmlns:tools="http://schemas.android.com/tools" package="com.tazkrtak.staff"> - + - - - - - + - + + + \ No newline at end of file diff --git a/app/src/main/java/com/tazkrtak/staff/App.kt b/app/src/main/java/com/tazkrtak/staff/App.kt new file mode 100644 index 0000000..555692e --- /dev/null +++ b/app/src/main/java/com/tazkrtak/staff/App.kt @@ -0,0 +1,17 @@ +package com.tazkrtak.staff + +import android.app.Application +import android.content.Context + +class App : Application() { + + override fun onCreate() { + super.onCreate() + appContext = applicationContext + } + + companion object { + var appContext: Context? = null + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/tazkrtak/staff/activities/LogInActivity.kt b/app/src/main/java/com/tazkrtak/staff/activities/LogInActivity.kt deleted file mode 100644 index b398284..0000000 --- a/app/src/main/java/com/tazkrtak/staff/activities/LogInActivity.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.tazkrtak.staff.activities - -import android.content.Intent -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import com.google.firebase.firestore.FirebaseFirestore -import com.tazkrtak.staff.R -import com.tazkrtak.staff.util.Hash -import com.tazkrtak.staff.util.SharedPrefHelper -import kotlinx.android.synthetic.main.activity_log_in.* - -class LogInActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - - super.onCreate(savedInstanceState) - - val intent = Intent(this, MainActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - - if (SharedPrefHelper.getString(this, "busId") != "") { - startActivity(intent) - finish() - } - - setContentView(R.layout.activity_log_in) - - login_button.setOnClickListener { - - val db = FirebaseFirestore.getInstance() - val id = bus_id_edit_text.text.toString() - val password = password_edit_text.text.toString() - - if (id.isEmpty()) { - bus_id_edit_text.error = getString(R.string.id_required) - return@setOnClickListener - } else if (password.isEmpty()) { - password_edit_text.error = getString(R.string.password_required) - return@setOnClickListener - } - - db.collection("buses") - .document(id) - .get().addOnSuccessListener { - if (!it.exists()) { - bus_id_edit_text.error = getString(R.string.id_error) - return@addOnSuccessListener - } - if (Hash.sha512(password) != it["password"].toString()) { - password_edit_text.error = getString(R.string.password_error) - return@addOnSuccessListener - } - - if (keep_signed_checkbox.isChecked) { - SharedPrefHelper.addString(this, "busId", id) - SharedPrefHelper.addString(this, "password", Hash.sha512(password)) - } - - startActivity(intent) - finish() - - } - } - - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tazkrtak/staff/activities/MainActivity.kt b/app/src/main/java/com/tazkrtak/staff/activities/MainActivity.kt index 0489a0d..88aac22 100644 --- a/app/src/main/java/com/tazkrtak/staff/activities/MainActivity.kt +++ b/app/src/main/java/com/tazkrtak/staff/activities/MainActivity.kt @@ -3,10 +3,9 @@ package com.tazkrtak.staff.activities import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import com.google.firebase.firestore.FirebaseFirestore import com.google.zxing.integration.android.IntentIntegrator import com.tazkrtak.staff.R -import com.tazkrtak.staff.util.SharedPrefHelper +import com.tazkrtak.staff.util.Auth import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { @@ -16,29 +15,6 @@ class MainActivity : AppCompatActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - val intent = Intent(this, LogInActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - - if (SharedPrefHelper.exists(this, "busId")) { - val savedId = SharedPrefHelper.getString(this, "busId") - val savedPassword = SharedPrefHelper.getString(this, "password") - - val db = FirebaseFirestore.getInstance() - db.collection("buses") - .document(savedId) - .get() - .addOnSuccessListener { - if (!it.exists() || it.data!!["password"] != savedPassword) { - SharedPrefHelper.removeString(this, "busId") - SharedPrefHelper.removeString(this, "password") - startActivity(intent) - finish() - return@addOnSuccessListener - } - } - } - scan_button.setOnClickListener { val integrator = IntentIntegrator(this) integrator.captureActivity = ScannerActivity::class.java @@ -47,5 +23,14 @@ class MainActivity : AppCompatActivity() { integrator.initiateScan() } + sign_out_button.setOnClickListener { + Auth.signOut() + val signInActivityIntent = Intent(this, SignInActivity::class.java) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(signInActivityIntent) + finish() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tazkrtak/staff/activities/SignInActivity.kt b/app/src/main/java/com/tazkrtak/staff/activities/SignInActivity.kt new file mode 100644 index 0000000..bbfa75d --- /dev/null +++ b/app/src/main/java/com/tazkrtak/staff/activities/SignInActivity.kt @@ -0,0 +1,66 @@ +package com.tazkrtak.staff.activities + +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.tazkrtak.staff.R +import com.tazkrtak.staff.util.Auth +import kotlinx.android.synthetic.main.activity_sign_in.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext + +class SignInActivity : AppCompatActivity(), CoroutineScope { + + override val coroutineContext: CoroutineContext + get() = Dispatchers.Main + job + + private lateinit var job: Job + + override fun onCreate(savedInstanceState: Bundle?) { + + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_sign_in) + + job = Job() + + sign_in_button.setOnClickListener { + + id_edit_text.error = null + id_edit_text.error = null + + val id = id_edit_text.text.toString() + val password = password_edit_text.text.toString() + + launch { + try { + Auth.signIn(id, password) + launchMainActivity() + } catch (e: Auth.AuthIdException) { + id_edit_text.error = e.message + } catch (e: Auth.AuthPasswordException) { + password_edit_text.error = e.message + } + } + + } + + } + + override fun onDestroy() { + job.cancel() + super.onDestroy() + } + + private fun launchMainActivity() { + val mainActivityIntent = Intent(this, MainActivity::class.java) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(mainActivityIntent) + finish() + } + +} + diff --git a/app/src/main/java/com/tazkrtak/staff/activities/SplashScreenActivity.kt b/app/src/main/java/com/tazkrtak/staff/activities/SplashScreenActivity.kt new file mode 100644 index 0000000..a3b837c --- /dev/null +++ b/app/src/main/java/com/tazkrtak/staff/activities/SplashScreenActivity.kt @@ -0,0 +1,49 @@ +package com.tazkrtak.staff.activities + +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.tazkrtak.staff.R +import com.tazkrtak.staff.util.Auth +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext + +class SplashScreenActivity : AppCompatActivity(), CoroutineScope { + + override val coroutineContext: CoroutineContext + get() = Dispatchers.Main + job + + private lateinit var job: Job + + override fun onCreate(savedInstanceState: Bundle?) { + + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_splash_screen) + job = Job() + + launch { + if (Auth.isSignedIn()) { + launchActivity(MainActivity::class.java) + } else { + launchActivity(SignInActivity::class.java) + } + } + + } + + override fun onDestroy() { + job.cancel() + super.onDestroy() + } + + private fun launchActivity(cls: Class) { + val activityIntent = Intent(this, cls) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(activityIntent) + finish() + } +} diff --git a/app/src/main/java/com/tazkrtak/staff/models/Account.kt b/app/src/main/java/com/tazkrtak/staff/models/Account.kt new file mode 100644 index 0000000..b323ec6 --- /dev/null +++ b/app/src/main/java/com/tazkrtak/staff/models/Account.kt @@ -0,0 +1,21 @@ +package com.tazkrtak.staff.models + +import androidx.core.text.isDigitsOnly + + +interface Account { + + val id: String? + val name: String? + val password: String? + val type: Type + + enum class Type { CONDUCTOR, COLLECTOR } + + companion object { + fun typeOf(id: String): Type { + return if (!id.isDigitsOnly()) Type.CONDUCTOR else Type.COLLECTOR + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/tazkrtak/staff/models/Bus.kt b/app/src/main/java/com/tazkrtak/staff/models/Bus.kt index e3b34bd..8bdbb03 100644 --- a/app/src/main/java/com/tazkrtak/staff/models/Bus.kt +++ b/app/src/main/java/com/tazkrtak/staff/models/Bus.kt @@ -3,6 +3,7 @@ package com.tazkrtak.staff.models data class Bus( val id: String? = null, val number: Int? = null, + val plateNumber: Int? = null, val startStation: String? = null, val endStation: String? = null, val ticketsPrices: ArrayList? = arrayListOf() diff --git a/app/src/main/java/com/tazkrtak/staff/models/Collector.kt b/app/src/main/java/com/tazkrtak/staff/models/Collector.kt new file mode 100644 index 0000000..48a78dc --- /dev/null +++ b/app/src/main/java/com/tazkrtak/staff/models/Collector.kt @@ -0,0 +1,9 @@ +package com.tazkrtak.staff.models + +data class Collector( + override val id: String? = null, + override val name: String? = null, + override val password: String? = null +) : Account { + override val type: Account.Type = Account.Type.COLLECTOR +} \ No newline at end of file diff --git a/app/src/main/java/com/tazkrtak/staff/models/Conductor.kt b/app/src/main/java/com/tazkrtak/staff/models/Conductor.kt new file mode 100644 index 0000000..4078a75 --- /dev/null +++ b/app/src/main/java/com/tazkrtak/staff/models/Conductor.kt @@ -0,0 +1,10 @@ +package com.tazkrtak.staff.models + +data class Conductor( + override val id: String? = null, + override val name: String? = null, + override val password: String? = null, + val bus: Bus? = null +) : Account { + override val type: Account.Type = Account.Type.CONDUCTOR +} \ No newline at end of file diff --git a/app/src/main/java/com/tazkrtak/staff/util/Auth.kt b/app/src/main/java/com/tazkrtak/staff/util/Auth.kt new file mode 100644 index 0000000..692e39d --- /dev/null +++ b/app/src/main/java/com/tazkrtak/staff/util/Auth.kt @@ -0,0 +1,86 @@ +package com.tazkrtak.staff.util + +import com.google.firebase.firestore.DocumentSnapshot +import com.google.firebase.firestore.FirebaseFirestore +import com.tazkrtak.staff.App +import com.tazkrtak.staff.R +import com.tazkrtak.staff.models.Account +import com.tazkrtak.staff.models.Collector +import com.tazkrtak.staff.models.Conductor +import kotlinx.coroutines.tasks.await + +object Auth { + + private const val ID = "id" + private const val PASSWORD = "password" + + var currentUser: Account? = null + + suspend fun isSignedIn(): Boolean { + if (currentUser == null) { + val data = getPrefs() + if (!data[ID].isNullOrEmpty() && !data[PASSWORD].isNullOrEmpty()) { + fetchData(data[ID].toString(), data[PASSWORD].toString()) + return true + } + } + return false + } + + suspend fun signIn(id: String, password: String) { + + if (id.isEmpty()) throw AuthIdException(getMessage(R.string.id_required)) + if (password.isEmpty()) throw AuthPasswordException(getMessage(R.string.password_required)) + + val hashedPassword = Hash.sha512(password) + fetchData(id, hashedPassword) + setPrefs(id, hashedPassword) + + } + + fun signOut() { + setPrefs(null, null) + currentUser = null + } + + private fun getAccountFromDocument(id: String, doc: DocumentSnapshot): Account? { + return if (Account.typeOf(id) == Account.Type.CONDUCTOR) { + doc.toObject(Conductor::class.java) + } else { + doc.toObject(Collector::class.java) + } + } + + private suspend fun fetchData(id: String, hashedPassword: String) { + + val db = FirebaseFirestore.getInstance() + val doc = db.collection("staff").document(id).get().await() + + if (!doc.exists()) throw AuthIdException(getMessage(R.string.id_error)) + val docPassword = doc.data?.get(PASSWORD)?.toString() + if (docPassword != hashedPassword) throw AuthPasswordException(getMessage(R.string.password_error)) + + currentUser = getAccountFromDocument(id, doc) + } + + private fun getPrefs(): Map { + return mapOf( + ID to SharedPref.getString(ID), + PASSWORD to SharedPref.getString(PASSWORD) + ) + } + + private fun setPrefs(id: String?, hashedPassword: String?) { + SharedPref.addString(ID, id) + SharedPref.addString(PASSWORD, hashedPassword) + } + + private fun getMessage(id: Int): String { + return App.appContext!!.getString(id) + } + + abstract class AuthException(message: String) : Exception(message) + class AuthIdException(message: String) : AuthException(message) + class AuthPasswordException(message: String) : AuthException(message) + +} diff --git a/app/src/main/java/com/tazkrtak/staff/util/Hash.kt b/app/src/main/java/com/tazkrtak/staff/util/Hash.kt index dc9bece..a96adca 100644 --- a/app/src/main/java/com/tazkrtak/staff/util/Hash.kt +++ b/app/src/main/java/com/tazkrtak/staff/util/Hash.kt @@ -4,13 +4,13 @@ import java.security.MessageDigest object Hash { - private const val HEX_CHARS = "0123456789ABCDEF" + private const val HEX_CHARS = "0123456789abcdef" fun sha512(input: String): String { return hash("SHA-512", input) } - private fun hash(type: String,input: String): String { + private fun hash(type: String, input: String): String { val bytes = MessageDigest .getInstance(type) diff --git a/app/src/main/java/com/tazkrtak/staff/util/SharedPref.kt b/app/src/main/java/com/tazkrtak/staff/util/SharedPref.kt new file mode 100644 index 0000000..2bec9e8 --- /dev/null +++ b/app/src/main/java/com/tazkrtak/staff/util/SharedPref.kt @@ -0,0 +1,25 @@ +package com.tazkrtak.staff.util + +import android.content.Context +import com.tazkrtak.staff.App + +object SharedPref { + + private const val SHARED_PREF = "PREF" + + fun addString(key: String, value: String?) { + val context = App.appContext!! + val sharedPref = context.getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE)!! + with(sharedPref.edit()) { + putString(key, value) + apply() + } + } + + fun getString(key: String?): String { + val context = App.appContext!! + val sharedPref = context.getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE) + return sharedPref.getString(key, "").toString() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/tazkrtak/staff/util/SharedPrefHelper.kt b/app/src/main/java/com/tazkrtak/staff/util/SharedPrefHelper.kt deleted file mode 100644 index c8544e4..0000000 --- a/app/src/main/java/com/tazkrtak/staff/util/SharedPrefHelper.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.tazkrtak.staff.util - -import android.content.Context - -object SharedPrefHelper { - - private const val SHARED_PREF = "SECRETS" - - fun addString(context: Context, key: String, value: String) { - val sharedPref = context.getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE) - with(sharedPref.edit()) { - putString(key, value) - apply() - } - } - - fun getString(context: Context, key: String): String { - val sharedPref = context.getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE) - return sharedPref.getString(key, "").toString() - } - - fun removeString(context: Context, key: String) { - val sharedPref = context.getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE) - with(sharedPref.edit()) { - remove(key) - apply() - } - } - - fun exists(context: Context, key: String): Boolean { - val sharedPref = context.getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE) - return sharedPref.contains(key) - } - -} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 537fbd6..7c39d1b 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -4,6 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" + android:orientation="vertical" tools:context=".activities.MainActivity">