Merge pull request 'Cookies login and logout.' (#103) from login into master
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/pulls/103
This commit is contained in:
Amine Louveau 2022-11-30 10:04:13 +00:00
commit f09f731d30
6 changed files with 149 additions and 66 deletions

View File

@ -869,7 +869,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
return true return true
} }
R.id.action_disconnect -> { R.id.action_disconnect -> {
appSettingsService.clearAll() CoroutineScope(Dispatchers.Main).launch {
repository.logout()
}
val intent = Intent(this, LoginActivity::class.java) val intent = Intent(this, LoginActivity::class.java)
this.startActivity(intent) this.startActivity(intent)
this@HomeActivity.finish() this@HomeActivity.finish()

View File

@ -1023,7 +1023,7 @@ class RepositoryTest {
@Test @Test
fun create_source() { fun create_source() {
coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns
SuccessResponse(true) SuccessResponse(true)
initializeRepository() initializeRepository()
@ -1045,7 +1045,6 @@ class RepositoryTest {
any(), any(),
any(), any(),
any(), any(),
any()
) )
} }
assertSame(true, response) assertSame(true, response)
@ -1053,7 +1052,7 @@ class RepositoryTest {
@Test @Test
fun create_source_but_response_fails() { fun create_source_but_response_fails() {
coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns
SuccessResponse(false) SuccessResponse(false)
initializeRepository() initializeRepository()
@ -1075,7 +1074,6 @@ class RepositoryTest {
any(), any(),
any(), any(),
any(), any(),
any()
) )
} }
assertSame(false, response) assertSame(false, response)
@ -1083,7 +1081,7 @@ class RepositoryTest {
@Test @Test
fun create_source_without_connection() { fun create_source_without_connection() {
coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns
SuccessResponse(true) SuccessResponse(true)
initializeRepository(MutableStateFlow(false)) initializeRepository(MutableStateFlow(false))
@ -1104,7 +1102,6 @@ class RepositoryTest {
any(), any(),
any(), any(),
any(), any(),
any(),
any() any()
) )
} }

View File

@ -23,6 +23,14 @@ class StatusAndData<T>(val success: Boolean, val data: T? = null) {
} }
} }
suspend fun responseOrSuccessIf404(r: HttpResponse): SuccessResponse {
return if (r.status === HttpStatusCode.NotFound) {
SuccessResponse(true)
} else {
maybeResponse(r)
}
}
suspend fun maybeResponse(r: HttpResponse): SuccessResponse { suspend fun maybeResponse(r: HttpResponse): SuccessResponse {
return if (r.status.isSuccess()) { return if (r.status.isSuccess()) {
r.body() r.body()

View File

@ -340,8 +340,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
url, url,
spout, spout,
tags, tags,
filter, filter
appSettingsService.getApiVersion()
).isSuccess == true ).isSuccess == true
} }
@ -373,12 +372,29 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap
val response = api.login() val response = api.login()
result = response.isSuccess == true result = response.isSuccess == true
} catch (cause: Throwable) { } catch (cause: Throwable) {
Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.updateRemote") Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.login")
} }
} }
return result return result
} }
suspend fun logout() {
if (isNetworkAvailable()) {
try {
val response = api.logout()
if (response.isSuccess) {
Napier.e("Couldn't logout.", tag = "RepositoryImpl.logout")
}
} catch (cause: Throwable) {
Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.logout")
} finally {
appSettingsService.clearAll()
}
} else {
appSettingsService.clearAll()
}
}
fun refreshLoginInformation(url: String, login: String, password: String) { fun refreshLoginInformation(url: String, login: String, password: String) {
appSettingsService.refreshLoginInformation(url, login, password) appSettingsService.refreshLoginInformation(url, login, password)
baseUrl = url baseUrl = url

View File

@ -2,11 +2,12 @@ package bou.amine.apps.readerforselfossv2.rest
import bou.amine.apps.readerforselfossv2.model.* import bou.amine.apps.readerforselfossv2.model.*
import bou.amine.apps.readerforselfossv2.service.AppSettingsService import bou.amine.apps.readerforselfossv2.service.AppSettingsService
import io.github.aakira.napier.Napier
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.* import io.ktor.client.plugins.*
import io.ktor.client.plugins.cache.* import io.ktor.client.plugins.cache.*
import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.cookies.*
import io.ktor.client.plugins.logging.* import io.ktor.client.plugins.logging.*
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.request.forms.* import io.ktor.client.request.forms.*
@ -20,7 +21,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
var client = createHttpClient() var client = createHttpClient()
private fun createHttpClient(): HttpClient { private fun createHttpClient(): HttpClient {
return HttpClient { val client = HttpClient {
install(ContentNegotiation) { install(ContentNegotiation) {
install(HttpCache) install(HttpCache)
json(Json { json(Json {
@ -32,7 +33,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
install(Logging) { install(Logging) {
logger = object : Logger { logger = object : Logger {
override fun log(message: String) { override fun log(message: String) {
appSettingsService.logApiCalls(message) Napier.d(message, tag = "LogApiCalls")
} }
} }
level = LogLevel.INFO level = LogLevel.INFO
@ -40,22 +41,26 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
install(HttpTimeout) { install(HttpTimeout) {
requestTimeoutMillis = appSettingsService.getApiTimeout() requestTimeoutMillis = appSettingsService.getApiTimeout()
} }
/* TODO: Auth as basic install(HttpCookies)
if (apiDetailsService.getUserName().isNotEmpty() && apiDetailsService.getPassword().isNotEmpty()) {
install(Auth) {
basic {
credentials {
BasicAuthCredentials(username = apiDetailsService.getUserName(), password = apiDetailsService.getPassword())
}
sendWithoutRequest {
true
}
}
}
}*/
expectSuccess = false expectSuccess = false
} }
client.plugin(HttpSend).intercept { request ->
val originalCall = execute(request)
if (originalCall.response.status == HttpStatusCode.Forbidden && shouldHavePostLogin() && hasLoginInfo()) {
Napier.i("Forbidden action, will try to login and retry", tag = "HttpSend")
if (login().isSuccess) {
Napier.i("Logged in worked", tag = "HttpSend")
execute(request)
}
originalCall
} else {
originalCall
}
}
return client
} }
fun url(path: String) = fun url(path: String) =
@ -66,11 +71,38 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
client = createHttpClient() client = createHttpClient()
} }
// Api version was introduces after the POST login, so when there is a version, it should be available
private fun shouldHavePostLogin() = appSettingsService.getApiVersion() != -1
private fun hasLoginInfo() = appSettingsService.getUserName() != null && appSettingsService.getPassword() != null
suspend fun login(): SuccessResponse = suspend fun login(): SuccessResponse =
maybeResponse(client.get(url("/login")) { if (shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) postLogin()
parameter("password", appSettingsService.getPassword()) } else {
}) getLogin()
}
private suspend fun getLogin() = maybeResponse(client.get(url("/login")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
})
private suspend fun postLogin() = maybeResponse(client.post(url("/login")) {
parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
})
private fun shouldHaveNewLogout() = appSettingsService.getApiVersion() >= 5 // We are missing 4.1.0
suspend fun logout(): SuccessResponse =
if (shouldHaveNewLogout()) {
doLogout()
} else {
maybeLogoutIfAvailable()
}
private suspend fun maybeLogoutIfAvailable() = responseOrSuccessIf404(client.get(url("/logout")))
private suspend fun doLogout() = maybeResponse(client.delete(url("/api/session/current")))
suspend fun getItems( suspend fun getItems(
type: String, type: String,
@ -82,80 +114,102 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
items: Int? = null items: Int? = null
): StatusAndData<List<SelfossModel.Item>> = ): StatusAndData<List<SelfossModel.Item>> =
bodyOrFailure(client.get(url("/items")) { bodyOrFailure(client.get(url("/items")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
parameter("type", type) }
parameter("tag", tag) parameter("type", type)
parameter("source", source) parameter("tag", tag)
parameter("search", search) parameter("source", source)
parameter("updatedsince", updatedSince) parameter("search", search)
parameter("items", items ?: appSettingsService.getItemsNumber()) parameter("updatedsince", updatedSince)
parameter("offset", offset) parameter("items", items ?: appSettingsService.getItemsNumber())
}) parameter("offset", offset)
})
suspend fun stats(): StatusAndData<SelfossModel.Stats> = suspend fun stats(): StatusAndData<SelfossModel.Stats> =
bodyOrFailure(client.get(url("/stats")) { bodyOrFailure(client.get(url("/stats")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}) }
})
suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> = suspend fun tags(): StatusAndData<List<SelfossModel.Tag>> =
bodyOrFailure(client.get(url("/tags")) { bodyOrFailure(client.get(url("/tags")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}) }
})
suspend fun update(): StatusAndData<String> = suspend fun update(): StatusAndData<String> =
bodyOrFailure(client.get(url("/update")) { bodyOrFailure(client.get(url("/update")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}) }
})
suspend fun spouts(): StatusAndData<Map<String, SelfossModel.Spout>> = suspend fun spouts(): StatusAndData<Map<String, SelfossModel.Spout>> =
bodyOrFailure(client.get(url("/sources/spouts")) { bodyOrFailure(client.get(url("/sources/spouts")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}) }
})
suspend fun sources(): StatusAndData<ArrayList<SelfossModel.Source>> = suspend fun sources(): StatusAndData<ArrayList<SelfossModel.Source>> =
bodyOrFailure(client.get(url("/sources/list")) { bodyOrFailure(client.get(url("/sources/list")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}) }
})
suspend fun version(): StatusAndData<SelfossModel.ApiVersion> = suspend fun version(): StatusAndData<SelfossModel.ApiVersion> =
bodyOrFailure(client.get(url("/api/about"))) bodyOrFailure(client.get(url("/api/about")))
suspend fun markAsRead(id: String): SuccessResponse = suspend fun markAsRead(id: String): SuccessResponse =
maybeResponse(client.post(url("/mark/$id")) { maybeResponse(client.post(url("/mark/$id")) {
parameter("username", appSettingsService.getUserName()) if (!shouldHavePostLogin()) {
parameter("password", appSettingsService.getPassword()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
}) })
suspend fun unmarkAsRead(id: String): SuccessResponse = suspend fun unmarkAsRead(id: String): SuccessResponse =
maybeResponse(client.post(url("/unmark/$id")) { maybeResponse(client.post(url("/unmark/$id")) {
parameter("username", appSettingsService.getUserName()) if (!shouldHavePostLogin()) {
parameter("password", appSettingsService.getPassword()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
}) })
suspend fun starr(id: String): SuccessResponse = suspend fun starr(id: String): SuccessResponse =
maybeResponse(client.post(url("/starr/$id")) { maybeResponse(client.post(url("/starr/$id")) {
parameter("username", appSettingsService.getUserName()) if (!shouldHavePostLogin()) {
parameter("password", appSettingsService.getPassword()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
}) })
suspend fun unstarr(id: String): SuccessResponse = suspend fun unstarr(id: String): SuccessResponse =
maybeResponse(client.post(url("/unstarr/$id")) { maybeResponse(client.post(url("/unstarr/$id")) {
parameter("username", appSettingsService.getUserName()) if (!shouldHavePostLogin()) {
parameter("password", appSettingsService.getPassword()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword())
}
}) })
suspend fun markAllAsRead(ids: List<String>): SuccessResponse = suspend fun markAllAsRead(ids: List<String>): SuccessResponse =
maybeResponse(client.submitForm( maybeResponse(client.submitForm(
url = url("/mark"), url = url("/mark"),
formParameters = Parameters.build { formParameters = Parameters.build {
append("username", appSettingsService.getUserName()) if (!shouldHavePostLogin()) {
append("password", appSettingsService.getPassword()) append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword())
}
ids.map { append("ids[]", it) } ids.map { append("ids[]", it) }
} }
)) ))
@ -165,18 +219,17 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
url: String, url: String,
spout: String, spout: String,
tags: String, tags: String,
filter: String, filter: String
version: Int
): SuccessResponse = ): SuccessResponse =
maybeResponse( maybeResponse(
if (version > 1) { if (appSettingsService.getApiVersion() > 1) {
createSource2(title, url, spout, tags, filter) createSource2(title, url, spout, tags, filter)
} else { } else {
createSource(title, url, spout, tags, filter) createSource(title, url, spout, tags, filter)
} }
) )
suspend fun createSource( private suspend fun createSource(
title: String, title: String,
url: String, url: String,
spout: String, spout: String,
@ -184,8 +237,13 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
filter: String filter: String
): HttpResponse = ): HttpResponse =
client.submitForm( client.submitForm(
url = url("/source?username=${appSettingsService.getUserName()}&password=${appSettingsService.getPassword()}"), url = url("/source"),
formParameters = Parameters.build { formParameters = Parameters.build {
// TODO: test this
if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword())
}
append("title", title) append("title", title)
append("url", url) append("url", url)
append("spout", spout) append("spout", spout)
@ -194,7 +252,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
} }
) )
suspend fun createSource2( private suspend fun createSource2(
title: String, title: String,
url: String, url: String,
spout: String, spout: String,
@ -202,8 +260,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
filter: String filter: String
): HttpResponse = ): HttpResponse =
client.submitForm( client.submitForm(
url = url("/source?username=${appSettingsService.getUserName()}&password=${appSettingsService.getPassword()}"), url = url("/source"),
formParameters = Parameters.build { formParameters = Parameters.build {
if (!shouldHavePostLogin()) {
append("username", appSettingsService.getUserName())
append("password", appSettingsService.getPassword())
}
append("title", title) append("title", title)
append("url", url) append("url", url)
append("spout", spout) append("spout", spout)
@ -214,7 +276,9 @@ class SelfossApi(private val appSettingsService: AppSettingsService) {
suspend fun deleteSource(id: Int): SuccessResponse = suspend fun deleteSource(id: Int): SuccessResponse =
maybeResponse(client.delete(url("/source/$id")) { maybeResponse(client.delete(url("/source/$id")) {
if (!shouldHavePostLogin()) {
parameter("username", appSettingsService.getUserName()) parameter("username", appSettingsService.getUserName())
parameter("password", appSettingsService.getPassword()) parameter("password", appSettingsService.getPassword())
}) }
})
} }

View File

@ -44,10 +44,6 @@ class AppSettingsService {
refreshUserSettings() refreshUserSettings()
} }
fun logApiCalls(message: String) {
Napier.d(message, tag = "LogApiCalls")
}
fun getApiVersion(): Int { fun getApiVersion(): Int {
if (_apiVersion == -1) { if (_apiVersion == -1) {
refreshApiVersion() refreshApiVersion()