#80: Display error messages for user when login failes

This commit is contained in:
Ryan Harg 2021-08-10 14:54:37 +02:00
parent ffcbabedce
commit 3aa37e5d3c
No known key found for this signature in database
GPG Key ID: 89106F3A84E6958C
8 changed files with 119 additions and 81 deletions

View File

@ -11,10 +11,7 @@ import androidx.lifecycle.lifecycleScope
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.databinding.ActivityLoginBinding
import audio.funkwhale.ffa.fragments.LoginDialog
import audio.funkwhale.ffa.utils.AppContext
import audio.funkwhale.ffa.utils.OAuth
import audio.funkwhale.ffa.utils.Userinfo
import audio.funkwhale.ffa.utils.log
import audio.funkwhale.ffa.utils.*
import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
@ -22,6 +19,7 @@ import com.github.kittinunf.result.Result
import com.preference.PowerPreference
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.koin.java.KoinJavaComponent.inject
data class FwCredentials(val token: String, val non_field_errors: List<String>?)
@ -74,27 +72,27 @@ class LoginActivity : AppCompatActivity() {
var hostname = hostname.text.toString().trim()
try {
if (hostname.isEmpty()) throw Exception(getString(R.string.login_error_hostname))
validateHostname(hostname, cleartext.isChecked)?.let {
hostnameField.error = it
return@setOnClickListener
}
Uri.parse(hostname).apply {
if (!cleartext.isChecked && scheme == "http") {
throw Exception(getString(R.string.login_error_hostname_https))
}
if (scheme == null) {
hostname = when (cleartext.isChecked) {
true -> "http://$hostname"
false -> "https://$hostname"
}
val uri = Uri.parse(hostname)
if (uri.scheme == null) {
hostname = when (cleartext.isChecked) {
true -> "http://$hostname"
false -> "https://$hostname"
}
}
hostnameField.error = ""
when (anonymous.isChecked) {
val fuelResult = when (anonymous.isChecked) {
false -> authedLogin(hostname)
true -> anonymousLogin(hostname)
}
hostnameField.error = mapFuelResultToError(fuelResult)
} catch (e: Exception) {
val message =
if (e.message?.isEmpty() == true) getString(R.string.login_error_hostname)
@ -106,56 +104,66 @@ class LoginActivity : AppCompatActivity() {
}
}
private fun mapFuelResultToError(fuelResult: FuelResult) = when {
fuelResult.httpStatus == 404 ->
getString(R.string.login_error_funkwhale_not_found)
!fuelResult.success ->
getString(R.string.login_error, fuelResult.message)
else -> ""
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
limitContainerWidth()
}
private fun authedLogin(hostname: String) {
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).setString("hostname", hostname)
private fun validateHostname(hostname: String, cleartext: Boolean): String? {
if (hostname.isEmpty()) {
return getString(R.string.login_error_hostname)
}
if (!cleartext && hostname.startsWith("http")) {
return getString(R.string.login_error_hostname_https)
}
return null
}
private fun authedLogin(hostname: String): FuelResult {
oAuth.init(hostname)
oAuth.register {
return oAuth.register {
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).setString("hostname", hostname)
oAuth.authorize(this)
}
}
private fun anonymousLogin(hostname: String) {
private fun anonymousLogin(hostname: String): FuelResult {
val dialog = LoginDialog().apply {
show(supportFragmentManager, "LoginDialog")
}
lifecycleScope.launch(Main) {
try {
val (_, _, result) = Fuel.get("$hostname/api/v1/tracks/")
.awaitObjectResponseResult(gsonDeserializerOf(FwCredentials::class.java))
val uri = "$hostname/api/v1/tracks/"
val (_, _, result) = runBlocking {
Fuel.get(uri).awaitObjectResponseResult(gsonDeserializerOf(FwCredentials::class.java))
}
when (result) {
is Result.Success -> {
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).apply {
setString("hostname", hostname)
setBoolean("anonymous", true)
}
dialog.dismiss()
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
finish()
}
is Result.Failure -> {
dialog.dismiss()
binding.hostnameField.error = result.error.localizedMessage
}
when (result) {
is Result.Success -> {
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).apply {
setString("hostname", hostname)
setBoolean("anonymous", true)
}
} catch (e: Exception) {
dialog.dismiss()
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
finish()
return FuelResult.ok()
}
val message =
if (e.message?.isEmpty() == true) getString(R.string.login_error_hostname)
else e.message
binding.hostnameField.error = message
is Result.Failure -> {
dialog.dismiss()
return FuelResult.from(result)
}
}
}

View File

@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment
import audio.funkwhale.ffa.R
import audio.funkwhale.ffa.fragments.BrowseFragment
import audio.funkwhale.ffa.repositories.Repository
import com.github.kittinunf.fuel.core.FuelError
import com.github.kittinunf.fuel.core.Request
import com.google.android.exoplayer2.offline.Download
import com.google.gson.Gson
@ -93,5 +94,9 @@ fun Request.authorize(context: Context, oAuth: OAuth): Request {
}
}
fun FuelError.formatResponseMessage(): String {
return "${response.statusCode}: ${response.url}"
}
fun Download.getMetadata(): DownloadInfo? =
Gson().fromJson(String(this.request.data), DownloadInfo::class.java)

View File

@ -0,0 +1,22 @@
package audio.funkwhale.ffa.utils
import com.github.kittinunf.fuel.core.FuelError
import com.github.kittinunf.result.Result
data class FuelResult(val httpStatus: Int? = null, val message: String? = null) {
val success: Boolean get() = httpStatus == 200
companion object {
fun ok() = FuelResult(200)
fun from(result: Result.Failure<FuelError>): FuelResult {
return FuelResult(result.error.response.statusCode, result.error.response.responseMessage)
}
fun failure(): FuelResult {
return FuelResult()
}
}
}

View File

@ -13,16 +13,7 @@ import com.github.kittinunf.fuel.gson.jsonBody
import com.github.kittinunf.result.Result
import com.preference.PowerPreference
import kotlinx.coroutines.runBlocking
import net.openid.appauth.AuthState
import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationRequest
import net.openid.appauth.AuthorizationResponse
import net.openid.appauth.AuthorizationService
import net.openid.appauth.AuthorizationServiceConfiguration
import net.openid.appauth.ClientSecretPost
import net.openid.appauth.RegistrationRequest
import net.openid.appauth.RegistrationResponse
import net.openid.appauth.ResponseTypeValues
import net.openid.appauth.*
fun AuthState.save() {
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).apply {
@ -84,8 +75,8 @@ class OAuth(private val authorizationServiceFactory: AuthorizationServiceFactory
state: AuthState,
context: Context
): Boolean {
if (state.needsTokenRefresh.also { it.log("needsTokenRefresh()") }
&& state.refreshToken != null) {
if (state.needsTokenRefresh.also { it.log("needsTokenRefresh()") } &&
state.refreshToken != null) {
val refreshRequest = state.createTokenRefreshRequest()
val auth = ClientSecretPost(state.clientSecret)
runBlocking {
@ -119,39 +110,46 @@ class OAuth(private val authorizationServiceFactory: AuthorizationServiceFactory
fun service(context: Context): AuthorizationService =
authorizationServiceFactory.create(context)
fun register(authState: AuthState? = null, callback: () -> Unit) {
fun register(authState: AuthState? = null, callback: () -> Unit): FuelResult {
(authState ?: state()).authorizationServiceConfiguration?.let { config ->
runBlocking {
val (_, _, result: Result<App, FuelError>) = Fuel.post(config.registrationEndpoint.toString())
val (_, _, result: Result<App, FuelError>) = runBlocking {
Fuel.post(config.registrationEndpoint.toString())
.header("Content-Type", "application/json")
.jsonBody(registrationBody())
.awaitObjectResponseResult(gsonDeserializerOf(App::class.java))
}
when (result) {
is Result.Success -> {
val app = result.get()
when (result) {
is Result.Success -> {
Log.i("OAuth", "OAuth client app created.")
val app = result.get()
val response = RegistrationResponse.Builder(registration()!!)
.setClientId(app.client_id)
.setClientSecret(app.client_secret)
.setClientIdIssuedAt(0)
.setClientSecretExpiresAt(null)
.build()
val response = RegistrationResponse.Builder(registration()!!)
.setClientId(app.client_id)
.setClientSecret(app.client_secret)
.setClientIdIssuedAt(0)
.setClientSecretExpiresAt(null)
.build()
state().apply {
update(response)
save()
callback()
}
state().apply {
update(response)
save()
callback()
return FuelResult.ok()
}
}
is Result.Failure -> {
result.log("register()")
}
is Result.Failure -> {
Log.i(
"OAuth", "Couldn't register client application ${result.error.formatResponseMessage()}"
)
return FuelResult.from(result)
}
}
}
Log.i("OAuth", "Missing AuthorizationServiceConfiguration")
return FuelResult.failure()
}
private fun registrationBody(): Map<String, String> {

View File

@ -7,6 +7,8 @@
<string name="login_password">Passwort</string>
<string name="login_submit">Anmelden</string>
<string name="login_logging_in">Einloggen</string>
<string name="login_error">Anmeldung fehlgeschlagen: %s</string>
<string name="login_error_funkwhale_not_found">Keine Funkwhale Pod gefunden</string>
<string name="login_error_hostname">Das ist keine gültige URL</string>
<string name="login_error_hostname_https">Der Zugriff auf den Funkwhale Server sollte über HTTPS erfolgen</string>
<string name="toolbar_search">Suche</string>

View File

@ -6,7 +6,7 @@
<color name="colorPrimary">#476d85</color>
<color name="colorPrimaryDark">#646568</color>
<color name="colorAccent">#d35400</color>
<color name="colorError">#fdcfbb</color>
<color name="colorError">#F75D28</color>
<color name="colorSelected">#dadada</color>
<color name="colorFavorite">#d35400</color>

View File

@ -9,6 +9,8 @@
<string name="login_password">Password</string>
<string name="login_submit">Log in</string>
<string name="login_logging_in">Logging in</string>
<string name="login_error">Login failed: %s</string>
<string name="login_error_funkwhale_not_found">No Funkwhale pod found</string>
<string name="login_error_hostname">This could not be understood as a valid URL</string>
<string name="login_error_hostname_https">The Funkwhale hostname should be secure through HTTPS</string>
<string name="login_error_userinfo">We could not retrieve information about your user</string>

View File

@ -0,0 +1 @@
Show error messages to user when login failes (#80)