From 44ee8e579b66a4b8510a14c330e1c990dcbbc940 Mon Sep 17 00:00:00 2001 From: Jorge Blacio Date: Fri, 4 Oct 2019 17:47:36 -0500 Subject: [PATCH] Added recovery code funtionality --- .../com/criptext/mail/db/ComposerLocalDB.kt | 2 - .../com/criptext/mail/db/EventLocalDB.kt | 1 - .../com/criptext/mail/db/MailboxLocalDB.kt | 1 - .../com/criptext/mail/db/SearchLocalDB.kt | 1 - .../mail/scenes/signin/SignInScene.kt | 31 ++++ .../scenes/signin/SignInSceneController.kt | 74 ++++++++- .../scenes/signin/data/SignInDataSource.kt | 16 ++ .../mail/scenes/signin/data/SignInRequest.kt | 2 + .../mail/scenes/signin/data/SignInResult.kt | 6 + .../signin/holders/LoginValidationHolder.kt | 7 + .../CheckUsernameAvailabilityWorker.kt | 2 + .../signin/workers/RecoveryCodeWorker.kt | 142 ++++++++++++++++++ .../scenes/signup/data/SignUpAPIClient.kt | 19 +++ .../utils/ui/GeneralDialogConfirmation.kt | 3 +- .../mail/utils/ui/GeneralDialogWithInput.kt | 47 ++++-- .../ui/GeneralDialogWithInputPassword.kt | 3 +- .../criptext/mail/utils/ui/data/DialogData.kt | 9 +- .../criptext/mail/utils/ui/data/DialogType.kt | 1 + .../mail/validation/AccountDataValidator.kt | 11 ++ .../res/layout/activity_login_validation.xml | 12 ++ .../res/layout/general_dialog_with_input.xml | 16 ++ src/main/res/values-es/strings.xml | 9 ++ src/main/res/values/strings.xml | 9 ++ 23 files changed, 402 insertions(+), 22 deletions(-) create mode 100644 src/main/kotlin/com/criptext/mail/scenes/signin/workers/RecoveryCodeWorker.kt diff --git a/src/main/kotlin/com/criptext/mail/db/ComposerLocalDB.kt b/src/main/kotlin/com/criptext/mail/db/ComposerLocalDB.kt index 543e423ea..c7219e2f3 100644 --- a/src/main/kotlin/com/criptext/mail/db/ComposerLocalDB.kt +++ b/src/main/kotlin/com/criptext/mail/db/ComposerLocalDB.kt @@ -22,7 +22,6 @@ class ComposerLocalDB(val contactDao: ContactDao, val emailDao: EmailDao, val fi val labels = emailLabelDao.getLabelsFromEmail(id) val contactsCC = emailContactDao.getContactsFromEmail(id, ContactTypes.CC) val contactsBCC = emailContactDao.getContactsFromEmail(id, ContactTypes.BCC) - val contactsFROM = emailContactDao.getContactsFromEmail(id, ContactTypes.FROM) val contactsTO = emailContactDao.getContactsFromEmail(id, ContactTypes.TO) val files = fileDao.getAttachmentsFromEmail(id) val fileKey = fileKeyDao.getAttachmentKeyFromEmail(id) @@ -65,7 +64,6 @@ class ComposerLocalDB(val contactDao: ContactDao, val emailDao: EmailDao, val fi labelDao.get(selectedLabel, activeAccount.id).id) else -1 val contactsCC = emailContactDao.getContactsFromEmail(id, ContactTypes.CC) val contactsBCC = emailContactDao.getContactsFromEmail(id, ContactTypes.BCC) - val contactsFROM = emailContactDao.getContactsFromEmail(id, ContactTypes.FROM) val contactsTO = emailContactDao.getContactsFromEmail(id, ContactTypes.TO) val files = fileDao.getAttachmentsFromEmail(id) val fileKey: FileKey? = fileKeyDao.getAttachmentKeyFromEmail(id) diff --git a/src/main/kotlin/com/criptext/mail/db/EventLocalDB.kt b/src/main/kotlin/com/criptext/mail/db/EventLocalDB.kt index aa5d8dbab..6f3e9f567 100644 --- a/src/main/kotlin/com/criptext/mail/db/EventLocalDB.kt +++ b/src/main/kotlin/com/criptext/mail/db/EventLocalDB.kt @@ -317,7 +317,6 @@ class EventLocalDB(private val db: AppDatabase, private val filesDir: File, priv val labels = db.emailLabelDao().getLabelsFromEmail(id) val contactsCC = db.emailContactDao().getContactsFromEmail(id, ContactTypes.CC) val contactsBCC = db.emailContactDao().getContactsFromEmail(id, ContactTypes.BCC) - val contactsFROM = db.emailContactDao().getContactsFromEmail(id, ContactTypes.FROM) val contactsTO = db.emailContactDao().getContactsFromEmail(id, ContactTypes.TO) val files = db.fileDao().getAttachmentsFromEmail(id) val fileKey = db.fileKeyDao().getAttachmentKeyFromEmail(id) diff --git a/src/main/kotlin/com/criptext/mail/db/MailboxLocalDB.kt b/src/main/kotlin/com/criptext/mail/db/MailboxLocalDB.kt index b087d491b..f441ad843 100644 --- a/src/main/kotlin/com/criptext/mail/db/MailboxLocalDB.kt +++ b/src/main/kotlin/com/criptext/mail/db/MailboxLocalDB.kt @@ -475,7 +475,6 @@ interface MailboxLocalDB { db.labelDao().get(selectedLabel, activeAccount.id).id) else -1 val contactsCC = db.emailContactDao().getContactsFromEmail(id, ContactTypes.CC) val contactsBCC = db.emailContactDao().getContactsFromEmail(id, ContactTypes.BCC) - val contactsFROM = db.emailContactDao().getContactsFromEmail(id, ContactTypes.FROM) val contactsTO = db.emailContactDao().getContactsFromEmail(id, ContactTypes.TO) val files = db.fileDao().getAttachmentsFromEmail(id) val fileKey: FileKey? = db.fileKeyDao().getAttachmentKeyFromEmail(id) diff --git a/src/main/kotlin/com/criptext/mail/db/SearchLocalDB.kt b/src/main/kotlin/com/criptext/mail/db/SearchLocalDB.kt index 3b26aeb23..c4f9acfa5 100644 --- a/src/main/kotlin/com/criptext/mail/db/SearchLocalDB.kt +++ b/src/main/kotlin/com/criptext/mail/db/SearchLocalDB.kt @@ -58,7 +58,6 @@ interface SearchLocalDB{ val labels = db.emailLabelDao().getLabelsFromEmail(id) val contactsCC = db.emailContactDao().getContactsFromEmail(id, ContactTypes.CC) val contactsBCC = db.emailContactDao().getContactsFromEmail(id, ContactTypes.BCC) - val contactsFROM = db.emailContactDao().getContactsFromEmail(id, ContactTypes.FROM) val contactsTO = db.emailContactDao().getContactsFromEmail(id, ContactTypes.TO) val files = db.fileDao().getAttachmentsFromEmail(id) val fileKey = db.fileKeyDao().getAttachmentKeyFromEmail(id) diff --git a/src/main/kotlin/com/criptext/mail/scenes/signin/SignInScene.kt b/src/main/kotlin/com/criptext/mail/scenes/signin/SignInScene.kt index 22cdf0b02..9b5adbb71 100644 --- a/src/main/kotlin/com/criptext/mail/scenes/signin/SignInScene.kt +++ b/src/main/kotlin/com/criptext/mail/scenes/signin/SignInScene.kt @@ -11,6 +11,7 @@ import com.criptext.mail.scenes.signup.holders.KeyGenerationHolder import com.criptext.mail.utils.UIMessage import com.criptext.mail.utils.getLocalizedUIMessage import com.criptext.mail.utils.ui.ForgotPasswordDialog +import com.criptext.mail.utils.ui.GeneralDialogWithInput import com.criptext.mail.utils.ui.GeneralMessageOkDialog import com.criptext.mail.utils.ui.RetrySyncAlertDialogNewDevice import com.criptext.mail.utils.ui.data.DialogData @@ -45,6 +46,10 @@ interface SignInScene { fun showDeviceCountRemaining(remaining: Int) fun showDeviceRemovalError() fun showToolbarCount(checked: Int) + fun showRecoveryCode() + fun showRecoveryDialogError(message: UIMessage?) + fun toggleLoadRecoveryCode(load: Boolean) + fun dismissRecoveryCodeDialog() var signInUIObserver: SignInSceneController.SignInUIObserver? @@ -71,6 +76,14 @@ interface SignInScene { ) ) + private val recoveryCodeDialog = GeneralDialogWithInput(view.context, + DialogData.DialogDataForRecoveryCode( + title = UIMessage(R.string.recovery_code_dialog_title), + message = UIMessage(R.string.recovery_code_dialog_message), + type = DialogType.RecoveryCode() + ) + ) + override var signInUIObserver: SignInSceneController.SignInUIObserver? = null set(value) { holder.uiObserver = value @@ -254,6 +267,24 @@ interface SignInScene { currentHolder.setToolbarCount(checked) } + override fun showRecoveryCode() { + recoveryCodeDialog.showDialog(signInUIObserver) + recoveryCodeDialog.editTextEmail.setHint(R.string.recovery_code_dialog_hint) + recoveryCodeDialog.editTextEmailLayout.hint = view.context.getLocalizedUIMessage(UIMessage(R.string.recovery_code_dialog_hint)) + } + + override fun showRecoveryDialogError(message: UIMessage?) { + recoveryCodeDialog.setEmailError(message) + } + + override fun toggleLoadRecoveryCode(load: Boolean) { + recoveryCodeDialog.toggleLoad(load) + } + + override fun dismissRecoveryCodeDialog() { + recoveryCodeDialog.dismiss() + } + override fun showKeyGenerationHolder() { viewGroup.removeAllViews() val keyGenerationLayout = View.inflate( diff --git a/src/main/kotlin/com/criptext/mail/scenes/signin/SignInSceneController.kt b/src/main/kotlin/com/criptext/mail/scenes/signin/SignInSceneController.kt index 34ad8ad49..b23100c39 100644 --- a/src/main/kotlin/com/criptext/mail/scenes/signin/SignInSceneController.kt +++ b/src/main/kotlin/com/criptext/mail/scenes/signin/SignInSceneController.kt @@ -24,6 +24,9 @@ import com.criptext.mail.utils.* import com.criptext.mail.utils.generaldatasource.data.GeneralDataSource import com.criptext.mail.utils.generaldatasource.data.GeneralRequest import com.criptext.mail.utils.generaldatasource.data.GeneralResult +import com.criptext.mail.utils.ui.data.DialogResult +import com.criptext.mail.utils.ui.data.DialogType +import com.criptext.mail.utils.uiobserver.UIObserver import com.criptext.mail.utils.virtuallist.VirtualListView import com.criptext.mail.validation.AccountDataValidator import com.criptext.mail.validation.FormData @@ -67,6 +70,7 @@ class SignInSceneController( is SignInResult.LinkStatus -> onLinkStatus(result) is SignInResult.FindDevices -> onFindDevices(result) is SignInResult.RemoveDevices -> onRemoveDevices(result) + is SignInResult.RecoveryCode -> onGenerateRecoveryCode(result) } } @@ -391,6 +395,27 @@ class SignInSceneController( } } + private fun onGenerateRecoveryCode(result: SignInResult.RecoveryCode){ + when(result){ + is SignInResult.RecoveryCode.Success -> { + if(result.isValidate) { + scene.dismissRecoveryCodeDialog() + scene.showKeyGenerationHolder() + } else { + scene.showRecoveryCode() + } + } + is SignInResult.RecoveryCode.Failure -> { + if(result.isValidate){ + scene.toggleLoadRecoveryCode(false) + scene.showRecoveryDialogError(result.message) + } else { + scene.showError(result.message) + } + } + } + } + private fun onUserAuthenticated(result: SignInResult.AuthenticateUser) { when (result) { is SignInResult.AuthenticateUser.Success -> { @@ -622,6 +647,51 @@ class SignInSceneController( } private val uiObserver = object : SignInUIObserver { + override fun onRecoveryCodeChangeListener(newPassword: String) { + + } + + override fun onGeneralOkButtonPressed(result: DialogResult) { + if(result is DialogResult.DialogWithInput && result.type is DialogType.RecoveryCode){ + scene.toggleLoadRecoveryCode(true) + val currentState = model.state as SignInLayoutState.LoginValidation + dataSource.submitRequest(SignInRequest.RecoveryCode(currentState.username, currentState.domain, model.ephemeralJwt, model.isMultiple, result.textInput)) + } + } + + override fun onOkButtonPressed(password: String) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun onCancelButtonPressed() { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun onLinkAuthConfirmed(untrustedDeviceInfo: DeviceInfo.UntrustedDeviceInfo) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun onLinkAuthDenied(untrustedDeviceInfo: DeviceInfo.UntrustedDeviceInfo) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun onSnackbarClicked() { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun onSyncAuthConfirmed(trustedDeviceInfo: DeviceInfo.TrustedDeviceInfo) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun onSyncAuthDenied(trustedDeviceInfo: DeviceInfo.TrustedDeviceInfo) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun onRecoveryCodeClicked() { + val currentState = model.state as SignInLayoutState.LoginValidation + dataSource.submitRequest(SignInRequest.RecoveryCode(currentState.username, currentState.domain, model.ephemeralJwt, model.isMultiple)) + } + override fun onTrashPressed(recipient: String, domain: String) { val checkedIndexes = Pair(mutableListOf(), mutableListOf()) model.devices.forEachIndexed { index, deviceItem -> @@ -921,14 +991,16 @@ class SignInSceneController( override fun requestPermissionResult(requestCode: Int, permissions: Array, grantResults: IntArray) { } - interface SignInUIObserver { + interface SignInUIObserver: UIObserver { fun onSubmitButtonClicked() fun toggleUsernameFocusState(isFocused: Boolean) fun onSignUpLabelClicked() fun userLoginReady() fun onCantAccessDeviceClick() + fun onRecoveryCodeClicked() fun onResendDeviceLinkAuth(username: String, domain: String) fun onPasswordChangeListener(newPassword: String) + fun onRecoveryCodeChangeListener(newPassword: String) fun onConfirmPasswordChangeListener(confirmPassword: String) fun onUsernameTextChanged(newUsername: String) fun onForgotPasswordClick() diff --git a/src/main/kotlin/com/criptext/mail/scenes/signin/data/SignInDataSource.kt b/src/main/kotlin/com/criptext/mail/scenes/signin/data/SignInDataSource.kt index 11bc445d4..bab721924 100644 --- a/src/main/kotlin/com/criptext/mail/scenes/signin/data/SignInDataSource.kt +++ b/src/main/kotlin/com/criptext/mail/scenes/signin/data/SignInDataSource.kt @@ -64,6 +64,22 @@ class SignInDataSource(override val runner: WorkRunner, flushResults(result) } ) + is SignInRequest.RecoveryCode -> RecoveryCodeWorker( + httpClient = httpClient, jwt = params.tempToken, + db = db, + code = params.code, + recipientId = params.recipientId, + domain = params.domain, + signUpDao = signUpDao, + keyGenerator = keyGenerator, + keyValueStorage = keyValueStorage, + accountDao = accountDao, + isMultiple = params.isMultiple, + messagingInstance = MessagingInstance.Default(), + publishFn = { result -> + flushResults(result) + } + ) is SignInRequest.LinkBegin -> LinkBeginWorker( httpClient = httpClient, username = params.username, domain = params.domain, diff --git a/src/main/kotlin/com/criptext/mail/scenes/signin/data/SignInRequest.kt b/src/main/kotlin/com/criptext/mail/scenes/signin/data/SignInRequest.kt index 8cfd4f536..b191e22fc 100644 --- a/src/main/kotlin/com/criptext/mail/scenes/signin/data/SignInRequest.kt +++ b/src/main/kotlin/com/criptext/mail/scenes/signin/data/SignInRequest.kt @@ -13,6 +13,8 @@ sealed class SignInRequest{ data class ForgotPassword(val username: String, val domain: String): SignInRequest() + data class RecoveryCode(val recipientId: String, val domain: String, val tempToken: String, val isMultiple: Boolean, val code: String? = null): SignInRequest() + data class LinkBegin(val username: String, val domain: String): SignInRequest() data class LinkAuth(val username: String, val ephemeralJwt: String, val domain: String, val password: String? = null): SignInRequest() diff --git a/src/main/kotlin/com/criptext/mail/scenes/signin/data/SignInResult.kt b/src/main/kotlin/com/criptext/mail/scenes/signin/data/SignInResult.kt index a073afc3b..db472bbf8 100644 --- a/src/main/kotlin/com/criptext/mail/scenes/signin/data/SignInResult.kt +++ b/src/main/kotlin/com/criptext/mail/scenes/signin/data/SignInResult.kt @@ -28,6 +28,12 @@ sealed class SignInResult { val exception: Exception): ForgotPassword() } + sealed class RecoveryCode: SignInResult() { + data class Success(val isValidate: Boolean): RecoveryCode() + data class Failure(val isValidate: Boolean, val message: UIMessage, + val exception: Exception): RecoveryCode() + } + sealed class LinkBegin: SignInResult() { data class Success(val ephemeralJwt: String, val hasTwoFA: Boolean): LinkBegin() data class NoDevicesAvailable(val message: UIMessage): LinkBegin() diff --git a/src/main/kotlin/com/criptext/mail/scenes/signin/holders/LoginValidationHolder.kt b/src/main/kotlin/com/criptext/mail/scenes/signin/holders/LoginValidationHolder.kt index 5abf1d450..67eb32584 100644 --- a/src/main/kotlin/com/criptext/mail/scenes/signin/holders/LoginValidationHolder.kt +++ b/src/main/kotlin/com/criptext/mail/scenes/signin/holders/LoginValidationHolder.kt @@ -25,6 +25,7 @@ class LoginValidationHolder( private var animLoading: AnimatorSet? = null private val rootLayout: View private val cantAccessDevice: TextView + private val recoveryCodeText: TextView private val textViewTitle: TextView private val textViewBody: TextView private val textViewRejected: TextView @@ -40,6 +41,7 @@ class LoginValidationHolder( init { rootLayout = view.findViewById(R.id.viewRoot) cantAccessDevice = view.findViewById(R.id.cant_access_device) + recoveryCodeText = view.findViewById(R.id.recovery_code) textViewTitle = view.findViewById(R.id.textViewTitle) textViewRejected = view.findViewById(R.id.device_rejected) textViewBody = view.findViewById(R.id.textViewBody) @@ -53,6 +55,7 @@ class LoginValidationHolder( if(initialState.hasTwoFA){ cantAccessDevice.visibility = View.GONE textViewTitle.text = view.context.getText(R.string.title_two_fa) + recoveryCodeText.visibility = View.VISIBLE } setListeners() @@ -145,6 +148,10 @@ class LoginValidationHolder( uiObserver?.onCantAccessDeviceClick() } + recoveryCodeText.setOnClickListener { + uiObserver?.onRecoveryCodeClicked() + } + buttonResend.setOnClickListener { uiObserver?.onResendDeviceLinkAuth(initialState.username, initialState.domain) setEnableButtons(false) diff --git a/src/main/kotlin/com/criptext/mail/scenes/signin/workers/CheckUsernameAvailabilityWorker.kt b/src/main/kotlin/com/criptext/mail/scenes/signin/workers/CheckUsernameAvailabilityWorker.kt index a8e3cd936..9901002e7 100644 --- a/src/main/kotlin/com/criptext/mail/scenes/signin/workers/CheckUsernameAvailabilityWorker.kt +++ b/src/main/kotlin/com/criptext/mail/scenes/signin/workers/CheckUsernameAvailabilityWorker.kt @@ -39,6 +39,8 @@ class CheckUsernameAvailabilityWorker(val httpClient: HttpClient, ServerCodes.Gone -> SignInResult.CheckUsernameAvailability.Failure(UIMessage(R.string.username_not_available)) ServerCodes.EnterpriseAccountSuspended -> SignInResult.CheckUsernameAvailability.Failure(UIMessage(R.string.account_suspended_sign_in_error)) + ServerCodes.BadRequest -> + SignInResult.CheckUsernameAvailability.Failure(UIMessage(R.string.username_invalid_error)) else -> SignInResult.CheckUsernameAvailability.Failure(UIMessage(R.string.server_bad_status, arrayOf(ex.errorCode))) } } else { diff --git a/src/main/kotlin/com/criptext/mail/scenes/signin/workers/RecoveryCodeWorker.kt b/src/main/kotlin/com/criptext/mail/scenes/signin/workers/RecoveryCodeWorker.kt new file mode 100644 index 000000000..848fe955f --- /dev/null +++ b/src/main/kotlin/com/criptext/mail/scenes/signin/workers/RecoveryCodeWorker.kt @@ -0,0 +1,142 @@ +package com.criptext.mail.scenes.signin.workers + +import com.criptext.mail.R +import com.criptext.mail.api.HttpClient +import com.criptext.mail.api.ServerErrorException +import com.criptext.mail.bgworker.BackgroundWorker +import com.criptext.mail.bgworker.ProgressReporter +import com.criptext.mail.db.AppDatabase +import com.criptext.mail.db.KeyValueStorage +import com.criptext.mail.db.dao.AccountDao +import com.criptext.mail.db.dao.SignUpDao +import com.criptext.mail.db.models.Account +import com.criptext.mail.db.models.Contact +import com.criptext.mail.scenes.signin.data.SignInResult +import com.criptext.mail.scenes.signin.data.SignInSession +import com.criptext.mail.scenes.signup.data.SignUpAPIClient +import com.criptext.mail.scenes.signup.data.StoreAccountTransaction +import com.criptext.mail.services.MessagingInstance +import com.criptext.mail.signal.SignalKeyGenerator +import com.criptext.mail.utils.AccountUtils +import com.criptext.mail.utils.ServerCodes +import com.criptext.mail.utils.UIMessage +import com.github.kittinunf.result.Result +import com.github.kittinunf.result.flatMap +import org.json.JSONObject + + +class RecoveryCodeWorker(val httpClient: HttpClient, + private val jwt: String, + private val recipientId: String, + private val domain: String, + private val code: String?, + private val db: AppDatabase, + signUpDao: SignUpDao, + private val keyGenerator: SignalKeyGenerator, + private val messagingInstance: MessagingInstance, + private val isMultiple: Boolean, + private val accountDao: AccountDao, + private val keyValueStorage: KeyValueStorage, + override val publishFn: (SignInResult) -> Unit) + : BackgroundWorker { + + private val apiClient = SignUpAPIClient(httpClient) + private val isValidate = code != null + private val storeAccountTransaction = StoreAccountTransaction(signUpDao, keyValueStorage, accountDao) + + override val canBeParallelized = true + + private val shouldKeepData: Boolean by lazy { + recipientId in AccountUtils.getLastLoggedAccounts(keyValueStorage) || + recipientId.plus("@$domain") in AccountUtils.getLastLoggedAccounts(keyValueStorage) + } + + override fun catchException(ex: Exception): SignInResult.RecoveryCode { + val message = createErrorMessage(ex) + if(!isValidate) { + if (ex is ServerErrorException && ex.errorCode == ServerCodes.BadRequest) + return SignInResult.RecoveryCode.Success(isValidate) + } + return SignInResult.RecoveryCode.Failure(isValidate, message, ex) + } + + override fun work(reporter: ProgressReporter): SignInResult.RecoveryCode? { + val result = Result.of { + if(!isValidate) { + apiClient.postTwoFAGenerateCode(recipientId, domain, jwt) + } else { + val json = JSONObject(apiClient.postValidateTwoFACode(recipientId, domain, jwt, code!!).body) + val deviceId = json.getInt("deviceId") + val name = json.getString("name") + if(!isMultiple){ + db.clearAllTables() + keyValueStorage.clearAll() + } + val signalPair = signalRegistrationOperation(deviceId, name) + storeAccountOperation(signalPair.first, signalPair.second) + } + } + + return when (result) { + is Result.Success ->{ + SignInResult.RecoveryCode.Success(isValidate) + } + is Result.Failure -> catchException(result.error) + } + + } + + private fun signalRegistrationOperation(deviceId: Int, name: String): Pair { + val recipient = if(domain != Contact.mainDomain) + recipientId.plus("@${domain}") + else + recipientId + val registrationBundles = keyGenerator.register(recipient, deviceId) + val privateBundle = registrationBundles.privateBundle + val account = Account(id = 0, recipientId = recipientId, deviceId = deviceId, + name = name, registrationId = privateBundle.registrationId, + identityKeyPairB64 = privateBundle.identityKeyPair, jwt = jwt, + signature = "", refreshToken = "", isActive = true, domain = domain, isLoggedIn = true, + autoBackupFrequency = 0, hasCloudBackup = false, lastTimeBackup = null, wifiOnly = true, + backupPassword = null) + return Pair(registrationBundles, account) + } + + private fun storeAccountOperation(registrationBundles: SignalKeyGenerator.RegistrationBundles, account: Account) { + val postKeyBundleStep = Runnable { + val response = apiClient.postKeybundle(bundle = registrationBundles.uploadBundle, + jwt = account.jwt) + val json = JSONObject(response.body) + account.jwt = json.getString("token") + account.refreshToken = json.getString("refreshToken") + if(messagingInstance.token != null) + apiClient.putFirebaseToken(messagingInstance.token ?: "", account.jwt) + accountDao.updateJwt(recipientId, domain, account.jwt) + accountDao.updateRefreshToken(recipientId, domain, account.refreshToken) + } + + storeAccountTransaction.run(account = account, + keyBundle = registrationBundles.privateBundle, + extraSteps = postKeyBundleStep, keepData = shouldKeepData, + isMultiple = isMultiple) + } + + override fun cancel() { + TODO("not implemented") //To change body of created functions use CRFile | Settings | CRFile Templates. + } + + private val createErrorMessage: (ex: Exception) -> UIMessage = { ex -> + when (ex) { + is ServerErrorException -> { + if (ex.errorCode == ServerCodes.MethodNotAllowed) + UIMessage(resId = R.string.title_warning_two_fa) + else if(ex.errorCode == ServerCodes.BadRequest) + UIMessage(resId = R.string.recovery_code_dialog_error) + else + UIMessage(resId = R.string.server_bad_status, args = arrayOf(ex.errorCode)) + } + else ->UIMessage(resId = R.string.forgot_password_error) + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/criptext/mail/scenes/signup/data/SignUpAPIClient.kt b/src/main/kotlin/com/criptext/mail/scenes/signup/data/SignUpAPIClient.kt index 73c601146..8fb330ca2 100644 --- a/src/main/kotlin/com/criptext/mail/scenes/signup/data/SignUpAPIClient.kt +++ b/src/main/kotlin/com/criptext/mail/scenes/signup/data/SignUpAPIClient.kt @@ -64,4 +64,23 @@ class SignUpAPIClient(private val httpClient: HttpClient) { return httpClient.put(path = "/keybundle/pushtoken", authToken = jwt, body = jsonPut) } + + fun postTwoFAGenerateCode(recipientId: String, domain: String, jwt: String): HttpResponseData { + val jsonPut = JSONObject() + jsonPut.put("recipientId", recipientId) + jsonPut.put("domain", domain) + return httpClient.post(path = "/user/2fa/generatecode", authToken = jwt, body = jsonPut) + } + + fun postValidateTwoFACode(recipientId: String, domain: String, jwt: String, code: String): HttpResponseData { + val json = JSONObject() + json.put("code", code) + json.put("recipientId", recipientId) + json.put("domain", domain) + return httpClient.post(path = "/user/2fa/validatecode", authToken = jwt, body = json) + } + + fun postKeybundle(bundle: PreKeyBundleShareData.UploadBundle, jwt: String): HttpResponseData { + return httpClient.post(path = "/keybundle", body = bundle.toJSON(), authToken = jwt) + } } diff --git a/src/main/kotlin/com/criptext/mail/utils/ui/GeneralDialogConfirmation.kt b/src/main/kotlin/com/criptext/mail/utils/ui/GeneralDialogConfirmation.kt index d8e3858ed..604485f4d 100644 --- a/src/main/kotlin/com/criptext/mail/utils/ui/GeneralDialogConfirmation.kt +++ b/src/main/kotlin/com/criptext/mail/utils/ui/GeneralDialogConfirmation.kt @@ -85,7 +85,8 @@ class GeneralDialogConfirmation(val context: Context, val data: DialogData.Dialo private fun createResult(): DialogResult { return when(data.type){ is DialogType.DeleteAccount, - is DialogType.ReplyToChange -> + is DialogType.ReplyToChange, + is DialogType.RecoveryCode -> DialogResult.DialogWithInput("", data.type) is DialogType.DeleteLabel, is DialogType.ManualSyncConfirmation, diff --git a/src/main/kotlin/com/criptext/mail/utils/ui/GeneralDialogWithInput.kt b/src/main/kotlin/com/criptext/mail/utils/ui/GeneralDialogWithInput.kt index 1ef80518b..33df8132c 100644 --- a/src/main/kotlin/com/criptext/mail/utils/ui/GeneralDialogWithInput.kt +++ b/src/main/kotlin/com/criptext/mail/utils/ui/GeneralDialogWithInput.kt @@ -24,12 +24,12 @@ import com.criptext.mail.validation.AccountDataValidator import com.criptext.mail.validation.FormData import com.google.android.material.textfield.TextInputLayout -class GeneralDialogWithInput(val context: Context, val data: DialogData.DialogDataForReplyToEmail) { +class GeneralDialogWithInput(val context: Context, val data: DialogData) { private var dialog: AlertDialog? = null private val res = context.resources - private lateinit var editTextEmail: AppCompatEditText - private lateinit var editTextEmailLayout: TextInputLayout + lateinit var editTextEmail: AppCompatEditText + lateinit var editTextEmailLayout: TextInputLayout private lateinit var btnOk: Button private lateinit var btnCancel: Button private lateinit var progressBar: ProgressBar @@ -62,16 +62,25 @@ class GeneralDialogWithInput(val context: Context, val data: DialogData.DialogDa assignButtonEvents(dialogView, newPasswordLoginDialog, observer) editTextEmail = dialogView.findViewById(R.id.input) editTextEmailLayout = dialogView.findViewById(R.id.input_layout) - if(data.replyToEmail != null) { - editTextEmail.setText(data.replyToEmail) - editTextEmail.setSelection(data.replyToEmail.length) - } + setData(dialogView) textListener() return newPasswordLoginDialog } + private fun setData(dialogView: View){ + if(data is DialogData.DialogDataForReplyToEmail) { + if (data.replyToEmail != null) { + editTextEmail.setText(data.replyToEmail) + editTextEmail.setSelection(data.replyToEmail.length) + } + } else if(data is DialogData.DialogDataForRecoveryCode){ + dialogView.findViewById(R.id.message).visibility = View.VISIBLE + dialogView.findViewById(R.id.message).text = context.getLocalizedUIMessage(data.message) + } + } + private fun assignButtonEvents(view: View, dialog: AlertDialog, observer: UIObserver?) { @@ -117,9 +126,9 @@ class GeneralDialogWithInput(val context: Context, val data: DialogData.DialogDa } private fun textListener() { - editTextEmail.addTextChangedListener( object : TextWatcher { + editTextEmail.addTextChangedListener(object : TextWatcher { override fun onTextChanged(text: CharSequence?, p1: Int, p2: Int, p3: Int) { - if(data.type is DialogType.ReplyToChange) { + if (data is DialogData.DialogDataForReplyToEmail) { val userInput = AccountDataValidator.validateEmailAddress(text.toString()) when (userInput) { is FormData.Valid -> { @@ -135,11 +144,28 @@ class GeneralDialogWithInput(val context: Context, val data: DialogData.DialogDa setEmailError(userInput.message) } } + } else if(data is DialogData.DialogDataForRecoveryCode){ + val userInput = AccountDataValidator.validateRecoveryCode(text.toString()) + when (userInput) { + is FormData.Valid -> { + if (!text.isNullOrEmpty()) { + setEmailError(null) + enableSaveButton() + } else { + disableSaveButton() + } + } + is FormData.Error -> { + disableSaveButton() + setEmailError(userInput.message) + } + } } } override fun afterTextChanged(p0: Editable?) { } + override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { } }) @@ -171,7 +197,8 @@ class GeneralDialogWithInput(val context: Context, val data: DialogData.DialogDa is DialogType.ManualSyncConfirmation, is DialogType.SwitchAccount -> DialogResult.DialogConfirmation(data.type) - is DialogType.ReplyToChange -> + is DialogType.ReplyToChange, + is DialogType.RecoveryCode -> DialogResult.DialogWithInput(editTextEmail.text.toString(), data.type) } } diff --git a/src/main/kotlin/com/criptext/mail/utils/ui/GeneralDialogWithInputPassword.kt b/src/main/kotlin/com/criptext/mail/utils/ui/GeneralDialogWithInputPassword.kt index ff691226f..1782b182d 100644 --- a/src/main/kotlin/com/criptext/mail/utils/ui/GeneralDialogWithInputPassword.kt +++ b/src/main/kotlin/com/criptext/mail/utils/ui/GeneralDialogWithInputPassword.kt @@ -107,7 +107,8 @@ class GeneralDialogWithInputPassword(val context: Context, val data: DialogData. private fun createResult(): DialogResult { return when(data.type){ is DialogType.DeleteAccount, - is DialogType.ReplyToChange -> + is DialogType.ReplyToChange, + is DialogType.RecoveryCode -> DialogResult.DialogWithInput(password.text.toString(), data.type) is DialogType.ManualSyncConfirmation, is DialogType.SignIn, diff --git a/src/main/kotlin/com/criptext/mail/utils/ui/data/DialogData.kt b/src/main/kotlin/com/criptext/mail/utils/ui/data/DialogData.kt index 8c6b01296..7e0599428 100644 --- a/src/main/kotlin/com/criptext/mail/utils/ui/data/DialogData.kt +++ b/src/main/kotlin/com/criptext/mail/utils/ui/data/DialogData.kt @@ -1,8 +1,9 @@ package com.criptext.mail.utils.ui.data import com.criptext.mail.utils.UIMessage -sealed class DialogData { - data class DialogMessageData(val title: UIMessage, val message: List, val type: DialogType, val onOkPress: (() -> Unit) = {}): DialogData() - data class DialogDataForReplyToEmail(val title: UIMessage, val replyToEmail: String?, val type: DialogType): DialogData() - data class DialogConfirmationData(val title: UIMessage, val message: List, val type: DialogType): DialogData() +sealed class DialogData(open val title: UIMessage, open val type: DialogType) { + data class DialogMessageData(override val title: UIMessage, val message: List, override val type: DialogType, val onOkPress: (() -> Unit) = {}): DialogData(title, type) + data class DialogDataForReplyToEmail(override val title: UIMessage, val replyToEmail: String?, override val type: DialogType): DialogData(title, type) + data class DialogDataForRecoveryCode(override val title: UIMessage, val message: UIMessage, override val type: DialogType): DialogData(title, type) + data class DialogConfirmationData(override val title: UIMessage, val message: List, override val type: DialogType): DialogData(title, type) } \ No newline at end of file diff --git a/src/main/kotlin/com/criptext/mail/utils/ui/data/DialogType.kt b/src/main/kotlin/com/criptext/mail/utils/ui/data/DialogType.kt index 39e67c80b..c56209d4f 100644 --- a/src/main/kotlin/com/criptext/mail/utils/ui/data/DialogType.kt +++ b/src/main/kotlin/com/criptext/mail/utils/ui/data/DialogType.kt @@ -4,6 +4,7 @@ sealed class DialogType{ class DeleteAccount: DialogType() class ManualSyncConfirmation: DialogType() class ReplyToChange: DialogType() + class RecoveryCode: DialogType() class SwitchAccount: DialogType() class SignIn: DialogType() class Message: DialogType() diff --git a/src/main/kotlin/com/criptext/mail/validation/AccountDataValidator.kt b/src/main/kotlin/com/criptext/mail/validation/AccountDataValidator.kt index 1a140be1e..fe7154019 100644 --- a/src/main/kotlin/com/criptext/mail/validation/AccountDataValidator.kt +++ b/src/main/kotlin/com/criptext/mail/validation/AccountDataValidator.kt @@ -57,4 +57,15 @@ object AccountDataValidator { else FormData.Valid(sanitizedValue) } + + fun validateRecoveryCode(code: String): FormData { + val sanitizedValue = code.trim() + + return if (sanitizedValue.length < 6) + FormData.Error(UIMessage(R.string.recovery_code_validation_error)) + else if (sanitizedValue.isEmpty()) + FormData.Error(UIMessage(R.string.recovery_code_validation_error_empty)) + else + FormData.Valid(sanitizedValue) + } } \ No newline at end of file diff --git a/src/main/res/layout/activity_login_validation.xml b/src/main/res/layout/activity_login_validation.xml index efa0f8dbd..61c7436bc 100644 --- a/src/main/res/layout/activity_login_validation.xml +++ b/src/main/res/layout/activity_login_validation.xml @@ -195,6 +195,18 @@ android:gravity="center_horizontal" android:text="@string/cant_access_device"/> + + \ No newline at end of file diff --git a/src/main/res/layout/general_dialog_with_input.xml b/src/main/res/layout/general_dialog_with_input.xml index 18b7ec2d6..9ddee45e4 100644 --- a/src/main/res/layout/general_dialog_with_input.xml +++ b/src/main/res/layout/general_dialog_with_input.xml @@ -18,6 +18,22 @@ android:textColor="?attr/criptextPrimaryTextColor" android:textSize="18sp"/> + + Reenviarlo ¿Acceder con contraseña? + + + Enviar Código de Recuperqación Inicio de Sesión Rechazado Dispositivo rechazado. @@ -590,4 +593,10 @@ Borrar Etiqueta ¿Estás seguro que quieres borrar la etiqueta: (%1$s)? Error borrando etiqueta. Por favor intenta de nuevo. + Código de Recuperación + Por favor revisa tu correo de recuperación e introduce el código que te hemos enviado. + Código + Código de Recuperación Incorrecto + El código de recuperación no puede ser menor a 6 dígitos. + El código de recuperación no puede estar vacio. diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 8da3f76b6..87f9116ef 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -250,6 +250,9 @@ Resend it Sign in with password? + + + Send Recovery Code Sign In Rejected Device rejected. @@ -590,4 +593,10 @@ Delete Label Are you sure you want to delete label: (%1$s)? Error deleting label. Please try again + Recovery Code + Please check your recovery email and enter the code that was sent to you. + Code + Incorrect Recovery Code + Recovery Code is at least 6 characters long. + Recovery Code cannot be empty.