#80: Display error messages for user when login failes
This commit is contained in:
parent
ffcbabedce
commit
3aa37e5d3c
|
@ -11,10 +11,7 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import audio.funkwhale.ffa.R
|
import audio.funkwhale.ffa.R
|
||||||
import audio.funkwhale.ffa.databinding.ActivityLoginBinding
|
import audio.funkwhale.ffa.databinding.ActivityLoginBinding
|
||||||
import audio.funkwhale.ffa.fragments.LoginDialog
|
import audio.funkwhale.ffa.fragments.LoginDialog
|
||||||
import audio.funkwhale.ffa.utils.AppContext
|
import audio.funkwhale.ffa.utils.*
|
||||||
import audio.funkwhale.ffa.utils.OAuth
|
|
||||||
import audio.funkwhale.ffa.utils.Userinfo
|
|
||||||
import audio.funkwhale.ffa.utils.log
|
|
||||||
import com.github.kittinunf.fuel.Fuel
|
import com.github.kittinunf.fuel.Fuel
|
||||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
||||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||||
|
@ -22,6 +19,7 @@ import com.github.kittinunf.result.Result
|
||||||
import com.preference.PowerPreference
|
import com.preference.PowerPreference
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.koin.java.KoinJavaComponent.inject
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
|
|
||||||
data class FwCredentials(val token: String, val non_field_errors: List<String>?)
|
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()
|
var hostname = hostname.text.toString().trim()
|
||||||
|
|
||||||
try {
|
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 {
|
val uri = Uri.parse(hostname)
|
||||||
if (!cleartext.isChecked && scheme == "http") {
|
if (uri.scheme == null) {
|
||||||
throw Exception(getString(R.string.login_error_hostname_https))
|
hostname = when (cleartext.isChecked) {
|
||||||
}
|
true -> "http://$hostname"
|
||||||
|
false -> "https://$hostname"
|
||||||
if (scheme == null) {
|
|
||||||
hostname = when (cleartext.isChecked) {
|
|
||||||
true -> "http://$hostname"
|
|
||||||
false -> "https://$hostname"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hostnameField.error = ""
|
hostnameField.error = ""
|
||||||
|
|
||||||
when (anonymous.isChecked) {
|
val fuelResult = when (anonymous.isChecked) {
|
||||||
false -> authedLogin(hostname)
|
false -> authedLogin(hostname)
|
||||||
true -> anonymousLogin(hostname)
|
true -> anonymousLogin(hostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hostnameField.error = mapFuelResultToError(fuelResult)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val message =
|
val message =
|
||||||
if (e.message?.isEmpty() == true) getString(R.string.login_error_hostname)
|
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) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
limitContainerWidth()
|
limitContainerWidth()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun authedLogin(hostname: String) {
|
private fun validateHostname(hostname: String, cleartext: Boolean): String? {
|
||||||
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).setString("hostname", hostname)
|
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.init(hostname)
|
||||||
oAuth.register {
|
return oAuth.register {
|
||||||
|
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).setString("hostname", hostname)
|
||||||
oAuth.authorize(this)
|
oAuth.authorize(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun anonymousLogin(hostname: String) {
|
private fun anonymousLogin(hostname: String): FuelResult {
|
||||||
val dialog = LoginDialog().apply {
|
val dialog = LoginDialog().apply {
|
||||||
show(supportFragmentManager, "LoginDialog")
|
show(supportFragmentManager, "LoginDialog")
|
||||||
}
|
}
|
||||||
|
|
||||||
lifecycleScope.launch(Main) {
|
val uri = "$hostname/api/v1/tracks/"
|
||||||
try {
|
val (_, _, result) = runBlocking {
|
||||||
val (_, _, result) = Fuel.get("$hostname/api/v1/tracks/")
|
Fuel.get(uri).awaitObjectResponseResult(gsonDeserializerOf(FwCredentials::class.java))
|
||||||
.awaitObjectResponseResult(gsonDeserializerOf(FwCredentials::class.java))
|
}
|
||||||
|
|
||||||
when (result) {
|
when (result) {
|
||||||
is Result.Success -> {
|
is Result.Success -> {
|
||||||
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).apply {
|
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).apply {
|
||||||
setString("hostname", hostname)
|
setString("hostname", hostname)
|
||||||
setBoolean("anonymous", true)
|
setBoolean("anonymous", true)
|
||||||
}
|
|
||||||
|
|
||||||
dialog.dismiss()
|
|
||||||
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
is Result.Failure -> {
|
|
||||||
dialog.dismiss()
|
|
||||||
|
|
||||||
binding.hostnameField.error = result.error.localizedMessage
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
|
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
|
||||||
|
finish()
|
||||||
|
return FuelResult.ok()
|
||||||
|
}
|
||||||
|
|
||||||
val message =
|
is Result.Failure -> {
|
||||||
if (e.message?.isEmpty() == true) getString(R.string.login_error_hostname)
|
dialog.dismiss()
|
||||||
else e.message
|
return FuelResult.from(result)
|
||||||
|
|
||||||
binding.hostnameField.error = message
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment
|
||||||
import audio.funkwhale.ffa.R
|
import audio.funkwhale.ffa.R
|
||||||
import audio.funkwhale.ffa.fragments.BrowseFragment
|
import audio.funkwhale.ffa.fragments.BrowseFragment
|
||||||
import audio.funkwhale.ffa.repositories.Repository
|
import audio.funkwhale.ffa.repositories.Repository
|
||||||
|
import com.github.kittinunf.fuel.core.FuelError
|
||||||
import com.github.kittinunf.fuel.core.Request
|
import com.github.kittinunf.fuel.core.Request
|
||||||
import com.google.android.exoplayer2.offline.Download
|
import com.google.android.exoplayer2.offline.Download
|
||||||
import com.google.gson.Gson
|
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? =
|
fun Download.getMetadata(): DownloadInfo? =
|
||||||
Gson().fromJson(String(this.request.data), DownloadInfo::class.java)
|
Gson().fromJson(String(this.request.data), DownloadInfo::class.java)
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,16 +13,7 @@ import com.github.kittinunf.fuel.gson.jsonBody
|
||||||
import com.github.kittinunf.result.Result
|
import com.github.kittinunf.result.Result
|
||||||
import com.preference.PowerPreference
|
import com.preference.PowerPreference
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import net.openid.appauth.AuthState
|
import net.openid.appauth.*
|
||||||
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
|
|
||||||
|
|
||||||
fun AuthState.save() {
|
fun AuthState.save() {
|
||||||
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).apply {
|
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).apply {
|
||||||
|
@ -84,8 +75,8 @@ class OAuth(private val authorizationServiceFactory: AuthorizationServiceFactory
|
||||||
state: AuthState,
|
state: AuthState,
|
||||||
context: Context
|
context: Context
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (state.needsTokenRefresh.also { it.log("needsTokenRefresh()") }
|
if (state.needsTokenRefresh.also { it.log("needsTokenRefresh()") } &&
|
||||||
&& state.refreshToken != null) {
|
state.refreshToken != null) {
|
||||||
val refreshRequest = state.createTokenRefreshRequest()
|
val refreshRequest = state.createTokenRefreshRequest()
|
||||||
val auth = ClientSecretPost(state.clientSecret)
|
val auth = ClientSecretPost(state.clientSecret)
|
||||||
runBlocking {
|
runBlocking {
|
||||||
|
@ -119,39 +110,46 @@ class OAuth(private val authorizationServiceFactory: AuthorizationServiceFactory
|
||||||
fun service(context: Context): AuthorizationService =
|
fun service(context: Context): AuthorizationService =
|
||||||
authorizationServiceFactory.create(context)
|
authorizationServiceFactory.create(context)
|
||||||
|
|
||||||
fun register(authState: AuthState? = null, callback: () -> Unit) {
|
fun register(authState: AuthState? = null, callback: () -> Unit): FuelResult {
|
||||||
(authState ?: state()).authorizationServiceConfiguration?.let { config ->
|
(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")
|
.header("Content-Type", "application/json")
|
||||||
.jsonBody(registrationBody())
|
.jsonBody(registrationBody())
|
||||||
.awaitObjectResponseResult(gsonDeserializerOf(App::class.java))
|
.awaitObjectResponseResult(gsonDeserializerOf(App::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
when (result) {
|
when (result) {
|
||||||
is Result.Success -> {
|
is Result.Success -> {
|
||||||
val app = result.get()
|
Log.i("OAuth", "OAuth client app created.")
|
||||||
|
val app = result.get()
|
||||||
|
|
||||||
val response = RegistrationResponse.Builder(registration()!!)
|
val response = RegistrationResponse.Builder(registration()!!)
|
||||||
.setClientId(app.client_id)
|
.setClientId(app.client_id)
|
||||||
.setClientSecret(app.client_secret)
|
.setClientSecret(app.client_secret)
|
||||||
.setClientIdIssuedAt(0)
|
.setClientIdIssuedAt(0)
|
||||||
.setClientSecretExpiresAt(null)
|
.setClientSecretExpiresAt(null)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
state().apply {
|
state().apply {
|
||||||
update(response)
|
update(response)
|
||||||
save()
|
save()
|
||||||
|
callback()
|
||||||
callback()
|
return FuelResult.ok()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
is Result.Failure -> {
|
is Result.Failure -> {
|
||||||
result.log("register()")
|
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> {
|
private fun registrationBody(): Map<String, String> {
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
<string name="login_password">Passwort</string>
|
<string name="login_password">Passwort</string>
|
||||||
<string name="login_submit">Anmelden</string>
|
<string name="login_submit">Anmelden</string>
|
||||||
<string name="login_logging_in">Einloggen</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">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="login_error_hostname_https">Der Zugriff auf den Funkwhale Server sollte über HTTPS erfolgen</string>
|
||||||
<string name="toolbar_search">Suche</string>
|
<string name="toolbar_search">Suche</string>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<color name="colorPrimary">#476d85</color>
|
<color name="colorPrimary">#476d85</color>
|
||||||
<color name="colorPrimaryDark">#646568</color>
|
<color name="colorPrimaryDark">#646568</color>
|
||||||
<color name="colorAccent">#d35400</color>
|
<color name="colorAccent">#d35400</color>
|
||||||
<color name="colorError">#fdcfbb</color>
|
<color name="colorError">#F75D28</color>
|
||||||
|
|
||||||
<color name="colorSelected">#dadada</color>
|
<color name="colorSelected">#dadada</color>
|
||||||
<color name="colorFavorite">#d35400</color>
|
<color name="colorFavorite">#d35400</color>
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
<string name="login_password">Password</string>
|
<string name="login_password">Password</string>
|
||||||
<string name="login_submit">Log in</string>
|
<string name="login_submit">Log in</string>
|
||||||
<string name="login_logging_in">Logging 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">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_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>
|
<string name="login_error_userinfo">We could not retrieve information about your user</string>
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Show error messages to user when login failes (#80)
|
Loading…
Reference in New Issue