From 38f16d5e7d3b8894c9dd2dde4f21b3fcd108b9bc Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Tue, 31 Aug 2021 15:51:42 +0300 Subject: [PATCH] 2fa support --- .../kotlin/com/meloda/fast/api/VKException.kt | 5 +- .../com/meloda/fast/api/network/VKUrls.kt | 9 +- .../api/network/datasource/AuthDataSource.kt | 2 + .../meloda/fast/api/network/repo/AuthRepo.kt | 4 + .../fast/api/network/response/AuthResponse.kt | 8 ++ .../fast/screens/login/LoginFragment.kt | 98 ++++++++++++------- .../fast/screens/login/LoginViewModel.kt | 78 ++++----------- app/src/main/res/layout/dialog_validation.xml | 83 ++++++++++++++++ app/src/main/res/values/strings.xml | 2 + 9 files changed, 192 insertions(+), 97 deletions(-) create mode 100644 app/src/main/res/layout/dialog_validation.xml diff --git a/app/src/main/kotlin/com/meloda/fast/api/VKException.kt b/app/src/main/kotlin/com/meloda/fast/api/VKException.kt index 1044a675..cfb87091 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/VKException.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/VKException.kt @@ -7,14 +7,11 @@ class VKException(var url: String = "", var description: String = "", var error: IOException(description) { var captcha: Pair? = 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;" } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/VKUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/VKUrls.kt index 5075877b..fd2ab868 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/VKUrls.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/VKUrls.kt @@ -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" } - } diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/AuthDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/network/datasource/AuthDataSource.kt index 4c9a06fb..68c5d6bc 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/AuthDataSource.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/datasource/AuthDataSource.kt @@ -7,4 +7,6 @@ class AuthDataSource @Inject constructor( private val repo: AuthRepo ) : AuthRepo { override suspend fun auth(param: Map) = repo.auth(param) + + override suspend fun sendSms(validationSid: String) = repo.sendSms(validationSid) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt index f05377ef..93c92fa7 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt @@ -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): Answer + @GET(VKUrls.Auth.sendSms) + suspend fun sendSms(@Query("sid") validationSid: String): Answer + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/response/AuthResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/network/response/AuthResponse.kt index dded6222..e9fe49b3 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/response/AuthResponse.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/response/AuthResponse.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt index 0060cc8f..653d8e33 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt @@ -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(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(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(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(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(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(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(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) } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt index 8a693924..afe5c5b5 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt @@ -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? = 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) : VKEvent() +object CodeSent : VKEvent() + data class SuccessAuth(val haveAuthorized: Boolean = true) : VKEvent() \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_validation.xml b/app/src/main/res/layout/dialog_validation.xml new file mode 100644 index 00000000..9e24ba07 --- /dev/null +++ b/app/src/main/res/layout/dialog_validation.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3a35b892..a2e533dc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -156,5 +156,7 @@ Login Conversations + Code + Input code from sms