2fa support

This commit is contained in:
2021-08-31 15:51:42 +03:00
parent f8b00e320f
commit 38f16d5e7d
9 changed files with 192 additions and 97 deletions
@@ -7,14 +7,11 @@ class VKException(var url: String = "", var description: String = "", var error:
IOException(description) {
var captcha: Pair<String, String>? = null
var redirectUri: String? = null
var validationSid: String? = null
var json: JSONObject? = null
override fun toString(): String {
return "url: $url;\n\nerror: $error; description: $description;"
return "error: $error; description: $description;"
}
}
@@ -2,16 +2,19 @@ package com.meloda.fast.api.network
object VKUrls {
const val OAUTH = "https://oauth.vk.com"
const val API = "https://api.vk.com/method"
object Auth {
const val directAuth = "https://oauth.vk.com/token"
const val directAuth = "$OAUTH/token"
const val sendSms = "$API/auth.validatePhone"
}
object Conversations {
const val get = "messages.getConversations"
const val get = "$API/messages.getConversations"
}
}
@@ -7,4 +7,6 @@ class AuthDataSource @Inject constructor(
private val repo: AuthRepo
) : AuthRepo {
override suspend fun auth(param: Map<String, String?>) = repo.auth(param)
override suspend fun sendSms(validationSid: String) = repo.sendSms(validationSid)
}
@@ -3,6 +3,7 @@ package com.meloda.fast.api.network.repo
import com.meloda.fast.api.network.VKUrls
import com.meloda.fast.api.network.response.ResponseAuthDirect
import com.meloda.fast.api.network.Answer
import com.meloda.fast.api.network.response.ResponseSendSms
import retrofit2.http.*
interface AuthRepo {
@@ -10,4 +11,7 @@ interface AuthRepo {
@GET(VKUrls.Auth.directAuth)
suspend fun auth(@QueryMap param: Map<String, String?>): Answer<ResponseAuthDirect>
@GET(VKUrls.Auth.sendSms)
suspend fun sendSms(@Query("sid") validationSid: String): Answer<ResponseSendSms>
}
@@ -10,4 +10,12 @@ data class ResponseAuthDirect(
@SerializedName("user_id") val userId: Int? = null,
@SerializedName("trusted_hash") val twoFaHash: String? = null,
@SerializedName("validation_sid") val validationSid: String? = null
) : Parcelable
@Parcelize
data class ResponseSendSms(
@SerializedName("sid") val validationSid: String?,
@SerializedName("delay") val delay: Int?,
@SerializedName("validation_type") val validationType: String?,
@SerializedName("validation_resend") val validationResend: String?
) : Parcelable
@@ -7,7 +7,6 @@ import android.view.View
import android.view.inputmethod.EditorInfo
import android.viewbinding.library.fragment.viewBinding
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.setFragmentResultListener
@@ -25,6 +24,7 @@ import com.meloda.fast.base.viewmodel.StartProgressEvent
import com.meloda.fast.base.viewmodel.StopProgressEvent
import com.meloda.fast.base.viewmodel.VKEvent
import com.meloda.fast.databinding.DialogCaptchaBinding
import com.meloda.fast.databinding.DialogValidationBinding
import com.meloda.fast.databinding.FragmentLoginBinding
import com.meloda.fast.screens.main.MainFragment
import com.meloda.fast.util.KeyboardUtils
@@ -45,7 +45,9 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
private var lastPassword: String = ""
private var errorTimer: Timer? = null
private var captchaInputLayout: TextInputLayout? = null
private var validationInputLayout: TextInputLayout? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -59,11 +61,6 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
setFragmentResultListener("validation") { _, bundle ->
lifecycleScope.launch { viewModel.getValidatedData(bundle) }
}
// showCaptchaDialog(
// "https://www.vets4pets.com/syssiteassets/species/cat/kitten/tiny-kitten-in-field.jpg?width=1040",
// ""
// )
}
override fun onEvent(event: VKEvent) {
@@ -72,8 +69,12 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
when (event) {
is ShowError -> showErrorSnackbar(event.errorDescription)
is CaptchaRequired -> showCaptchaDialog(event.captcha.first, event.captcha.second)
is ValidationRequired -> goToValidation()
CodeSent -> showValidationDialog()
is ValidationRequired -> showValidationRequired()
is SuccessAuth -> goToMain(event.haveAuthorized)
StartProgressEvent -> onProgressStarted()
StopProgressEvent -> onProgressStopped()
}
@@ -151,19 +152,19 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
KeyboardUtils.hideKeyboardFrom(requireView().findFocus())
lifecycleScope.launch {
viewModel.login(
login = loginString,
password = passwordString
)
}
viewModel.login(
login = loginString,
password = passwordString
)
}
// TODO: 7/27/2021 extract strings to resources
private fun validateInputData(
loginString: String?,
passwordString: String?,
captchaCode: String? = null
captchaCode: String? = null,
validationCode: String? = null
): Boolean {
var isValidated = true
@@ -182,6 +183,11 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
setError("Input code", captchaInputLayout!!)
}
if (validationCode?.isEmpty() == true && validationInputLayout != null) {
isValidated = false
setError("Input code", validationInputLayout!!)
}
return isValidated
}
@@ -237,17 +243,52 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
dialog.dismiss()
lifecycleScope.launch {
viewModel.login(
login = lastLogin,
password = lastPassword,
captcha = captchaSid to captchaCode
)
}
viewModel.login(
login = lastLogin,
password = lastPassword,
captcha = captchaSid to captchaCode
)
}
captchaBinding.cancel.setOnClickListener { dialog.dismiss() }
}
private fun showValidationDialog() {
val validationBinding = DialogValidationBinding.inflate(layoutInflater, null, false)
validationInputLayout = validationBinding.codeLayout
val builder = AlertDialog.Builder(requireContext())
.setView(validationBinding.root)
.setCancelable(false)
.setTitle(R.string.input_validation_code)
val dialog = builder.show()
validationBinding.ok.setOnClickListener {
val validationCode = validationBinding.codeInput.text.toString().trim()
if (!validateInputData(
loginString = null,
passwordString = null,
validationCode = validationCode
)
) return@setOnClickListener
dialog.dismiss()
viewModel.login(
login = lastLogin,
password = lastPassword,
twoFaCode = validationCode
)
}
validationBinding.cancel.setOnClickListener { dialog.dismiss() }
}
// TODO: 8/31/2021 show snackbar
private fun showValidationRequired() {
}
private fun showErrorSnackbar(errorDescription: String) {
val snackbar = Snackbar.make(
requireView(),
@@ -259,19 +300,10 @@ class LoginFragment : BaseVMFragment<LoginViewModel>(R.layout.fragment_login) {
snackbar.show()
}
private fun goToValidation() {
// findNavController().navigate(
// R.id.toValidation,
// bundleOf("redirectUrl" to redirectUrl)
// )
}
private fun goToMain(haveAuthorized: Boolean) = lifecycleScope.launch {
if (haveAuthorized) delay(500)
private fun goToMain(haveAuthorized: Boolean) {
lifecycleScope.launch {
if (haveAuthorized) delay(500)
findNavController().navigate(R.id.toMain)
}
findNavController().navigate(R.id.toMain)
}
}
@@ -26,13 +26,12 @@ class LoginViewModel @Inject constructor(
private val repo: AuthRepo
) : BaseViewModel() {
suspend fun login(
fun login(
login: String,
password: String,
twoFa: Boolean = false,
twoFaCode: String? = null,
captcha: Pair<String, String>? = null
) {
) = viewModelScope.launch {
makeJob(
{
repo.auth(
@@ -43,7 +42,7 @@ class LoginViewModel @Inject constructor(
username = login,
password = password,
scope = VKAuth.scope,
twoFaForceSms = twoFa,
twoFaForceSms = true,
twoFaCode = twoFaCode,
captchaSid = captcha?.first,
captchaKey = captcha?.second
@@ -63,13 +62,18 @@ class LoginViewModel @Inject constructor(
},
onError = {
checkErrors(it)
if (it !is VKException) return@makeJob
if (VKUtil.isValidationRequired(it)) {
sendEvent(ValidationRequired(validationSid = it.validationSid))
} else if (VKUtil.isCaptchaRequired(it) && it.captcha != null) {
sendEvent(CaptchaRequired(it.captcha!!.first to it.captcha!!.second))
it.validationSid?.let { sid ->
sendEvent(ValidationRequired(validationSid = sid))
sendSms(sid)
}
} else if (VKUtil.isCaptchaRequired(it)) {
it.captcha?.let { captcha ->
sendEvent(CaptchaRequired(captcha.first to captcha.second))
}
}
},
onStart = { sendEvent(StartProgressEvent) },
@@ -77,49 +81,12 @@ class LoginViewModel @Inject constructor(
)
}
@Suppress("MoveVariableDeclarationIntoWhen")
private fun checkResponse(response: JSONObject) {
viewModelScope.launch(Dispatchers.Default) {
if (response.has("error")) {
sendEvent(StopProgressEvent)
val errorString = response.optString("error")
val errorDescription = response.optString("error_description")
// TODO: 7/27/2021 use this with localized resources
// val errorType = response.optString("error_type")
when (errorString) {
"need_validation" -> {
val redirectUrl = response.optString("redirect_uri")
tasksEventChannel.send(ValidationRequired(redirectUrl))
}
"need_captcha" -> {
val captchaImage = response.optString("captcha_img")
val captchaSid = response.optString("captcha_sid")
Log.d("CAPTCHA", "captchaImage: $captchaImage")
tasksEventChannel.send(ShowCaptchaDialog(captchaImage, captchaSid))
}
else -> {
tasksEventChannel.send(ShowError(errorDescription))
}
}
} else {
delay(1500)
sendEvent(StopProgressEvent)
val userId = response.optInt("user_id", -1)
val accessToken = response.optString("access_token")
UserConfig.accessToken = accessToken
UserConfig.userId = userId
tasksEventChannel.send(SuccessAuth())
}
}
fun sendSms(validationSid: String) = viewModelScope.launch {
makeJob({ repo.sendSms(validationSid) },
onAnswer = { sendEvent(CodeSent) },
onError = {},
onStart = {},
onEnd = {})
}
suspend fun getValidatedData(bundle: Bundle) {
@@ -135,13 +102,10 @@ class LoginViewModel @Inject constructor(
}
data class ShowError(val errorDescription: String) : VKEvent()
data class ShowCaptchaDialog(val captchaImage: String, val captchaSid: String) : VKEvent()
data class ValidationRequired(
val redirectUrl: String? = null,
val validationSid: String? = null
) : VKEvent()
data class ValidationRequired(val validationSid: String) : VKEvent()
data class CaptchaRequired(val captcha: Pair<String, String>) : VKEvent()
object CodeSent : VKEvent()
data class SuccessAuth(val haveAuthorized: Boolean = true) : VKEvent()
@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/activity_horizontal_margin">
<LinearLayout
android:id="@+id/codeContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/codeImage"
style="@style/AppTheme.Login.EditText.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:src="@drawable/ic_security"
app:tint="?colorAccent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/codeLayout"
style="@style/Widget.TextInputLayout.NoError.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/codeInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/code_hint"
android:imeOptions="actionGo"
android:inputType="number"
android:lines="1"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/cancel"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:layout_weight="1"
android:text="@android:string/cancel"
android:textColor="?colorAction"
app:elevation="0dp"
app:rippleColor="?colorActionRipple" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ok"
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:backgroundTint="?colorAction"
android:text="@android:string/ok"
android:textColor="?colorActionContentPrimary"
app:elevation="0dp"
app:rippleColor="?colorActionRipple" />
</LinearLayout>
</LinearLayout>
</layout>
+2
View File
@@ -156,5 +156,7 @@
<string name="login_hint">Login</string>
<string name="conversations">Conversations</string>
<string name="code_hint">Code</string>
<string name="input_validation_code">Input code from sms</string>
</resources>