diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt index 814b335..0fedc3e 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt @@ -869,7 +869,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar return true } R.id.action_disconnect -> { - appSettingsService.clearAll() + CoroutineScope(Dispatchers.Main).launch { + repository.logout() + } val intent = Intent(this, LoginActivity::class.java) this.startActivity(intent) this@HomeActivity.finish() diff --git a/androidApp/src/test/kotlin/RepositoryTest.kt b/androidApp/src/test/kotlin/RepositoryTest.kt index 2f219bf..feb251a 100644 --- a/androidApp/src/test/kotlin/RepositoryTest.kt +++ b/androidApp/src/test/kotlin/RepositoryTest.kt @@ -1023,7 +1023,7 @@ class RepositoryTest { @Test fun create_source() { - coEvery { api.createSourceForVersion(any(), any(), any(), any(), any(), any()) } returns + coEvery { api.createSourceForVersion(any(), any(), any(), any(), any()) } returns SuccessResponse(true) initializeRepository() @@ -1045,7 +1045,6 @@ class RepositoryTest { any(), any(), any(), - any() ) } assertSame(true, response) @@ -1053,7 +1052,7 @@ class RepositoryTest { @Test 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) initializeRepository() @@ -1075,7 +1074,6 @@ class RepositoryTest { any(), any(), any(), - any() ) } assertSame(false, response) @@ -1083,7 +1081,7 @@ class RepositoryTest { @Test 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) initializeRepository(MutableStateFlow(false)) @@ -1104,7 +1102,6 @@ class RepositoryTest { any(), any(), any(), - any(), any() ) } diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/model/ResultModel.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/model/ResultModel.kt index 1234c68..7c56467 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/model/ResultModel.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/model/ResultModel.kt @@ -23,6 +23,14 @@ class StatusAndData(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 { return if (r.status.isSuccess()) { r.body() diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt index 5a81ba2..1bf4735 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt @@ -340,8 +340,7 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap url, spout, tags, - filter, - appSettingsService.getApiVersion() + filter ).isSuccess == true } @@ -373,12 +372,29 @@ class Repository(private val api: SelfossApi, private val appSettingsService: Ap val response = api.login() result = response.isSuccess == true } catch (cause: Throwable) { - Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.updateRemote") + Napier.e(cause.stackTraceToString(), tag = "RepositoryImpl.login") } } 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) { appSettingsService.refreshLoginInformation(url, login, password) baseUrl = url diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt index cf609ac..1c216c8 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt @@ -2,11 +2,12 @@ package bou.amine.apps.readerforselfossv2.rest import bou.amine.apps.readerforselfossv2.model.* import bou.amine.apps.readerforselfossv2.service.AppSettingsService +import io.github.aakira.napier.Napier import io.ktor.client.* -import io.ktor.client.call.* import io.ktor.client.plugins.* import io.ktor.client.plugins.cache.* import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.plugins.cookies.* import io.ktor.client.plugins.logging.* import io.ktor.client.request.* import io.ktor.client.request.forms.* @@ -20,7 +21,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { var client = createHttpClient() private fun createHttpClient(): HttpClient { - return HttpClient { + val client = HttpClient { install(ContentNegotiation) { install(HttpCache) json(Json { @@ -32,7 +33,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { install(Logging) { logger = object : Logger { override fun log(message: String) { - appSettingsService.logApiCalls(message) + Napier.d(message, tag = "LogApiCalls") } } level = LogLevel.INFO @@ -40,22 +41,26 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { install(HttpTimeout) { requestTimeoutMillis = appSettingsService.getApiTimeout() } - /* TODO: Auth as basic - if (apiDetailsService.getUserName().isNotEmpty() && apiDetailsService.getPassword().isNotEmpty()) { - - install(Auth) { - basic { - credentials { - BasicAuthCredentials(username = apiDetailsService.getUserName(), password = apiDetailsService.getPassword()) - } - sendWithoutRequest { - true - } - } - } - }*/ + install(HttpCookies) 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) = @@ -66,11 +71,38 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { 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 = - maybeResponse(client.get(url("/login")) { - parameter("username", appSettingsService.getUserName()) - parameter("password", appSettingsService.getPassword()) - }) + if (shouldHavePostLogin()) { + postLogin() + } 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( type: String, @@ -82,80 +114,102 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { items: Int? = null ): StatusAndData> = bodyOrFailure(client.get(url("/items")) { + if (!shouldHavePostLogin()) { parameter("username", appSettingsService.getUserName()) parameter("password", appSettingsService.getPassword()) - parameter("type", type) - parameter("tag", tag) - parameter("source", source) - parameter("search", search) - parameter("updatedsince", updatedSince) - parameter("items", items ?: appSettingsService.getItemsNumber()) - parameter("offset", offset) - }) + } + parameter("type", type) + parameter("tag", tag) + parameter("source", source) + parameter("search", search) + parameter("updatedsince", updatedSince) + parameter("items", items ?: appSettingsService.getItemsNumber()) + parameter("offset", offset) + }) suspend fun stats(): StatusAndData = bodyOrFailure(client.get(url("/stats")) { + if (!shouldHavePostLogin()) { parameter("username", appSettingsService.getUserName()) parameter("password", appSettingsService.getPassword()) - }) + } + }) suspend fun tags(): StatusAndData> = bodyOrFailure(client.get(url("/tags")) { + if (!shouldHavePostLogin()) { parameter("username", appSettingsService.getUserName()) parameter("password", appSettingsService.getPassword()) - }) + } + }) suspend fun update(): StatusAndData = bodyOrFailure(client.get(url("/update")) { + if (!shouldHavePostLogin()) { parameter("username", appSettingsService.getUserName()) parameter("password", appSettingsService.getPassword()) - }) + } + }) suspend fun spouts(): StatusAndData> = bodyOrFailure(client.get(url("/sources/spouts")) { + if (!shouldHavePostLogin()) { parameter("username", appSettingsService.getUserName()) parameter("password", appSettingsService.getPassword()) - }) + } + }) suspend fun sources(): StatusAndData> = bodyOrFailure(client.get(url("/sources/list")) { + if (!shouldHavePostLogin()) { parameter("username", appSettingsService.getUserName()) parameter("password", appSettingsService.getPassword()) - }) + } + }) suspend fun version(): StatusAndData = bodyOrFailure(client.get(url("/api/about"))) suspend fun markAsRead(id: String): SuccessResponse = maybeResponse(client.post(url("/mark/$id")) { - parameter("username", appSettingsService.getUserName()) - parameter("password", appSettingsService.getPassword()) + if (!shouldHavePostLogin()) { + parameter("username", appSettingsService.getUserName()) + parameter("password", appSettingsService.getPassword()) + } }) suspend fun unmarkAsRead(id: String): SuccessResponse = maybeResponse(client.post(url("/unmark/$id")) { - parameter("username", appSettingsService.getUserName()) - parameter("password", appSettingsService.getPassword()) + if (!shouldHavePostLogin()) { + parameter("username", appSettingsService.getUserName()) + parameter("password", appSettingsService.getPassword()) + } }) suspend fun starr(id: String): SuccessResponse = maybeResponse(client.post(url("/starr/$id")) { - parameter("username", appSettingsService.getUserName()) - parameter("password", appSettingsService.getPassword()) + if (!shouldHavePostLogin()) { + parameter("username", appSettingsService.getUserName()) + parameter("password", appSettingsService.getPassword()) + } }) suspend fun unstarr(id: String): SuccessResponse = maybeResponse(client.post(url("/unstarr/$id")) { - parameter("username", appSettingsService.getUserName()) - parameter("password", appSettingsService.getPassword()) + if (!shouldHavePostLogin()) { + parameter("username", appSettingsService.getUserName()) + parameter("password", appSettingsService.getPassword()) + } }) suspend fun markAllAsRead(ids: List): SuccessResponse = maybeResponse(client.submitForm( url = url("/mark"), formParameters = Parameters.build { - append("username", appSettingsService.getUserName()) - append("password", appSettingsService.getPassword()) + if (!shouldHavePostLogin()) { + append("username", appSettingsService.getUserName()) + append("password", appSettingsService.getPassword()) + } ids.map { append("ids[]", it) } } )) @@ -165,18 +219,17 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { url: String, spout: String, tags: String, - filter: String, - version: Int + filter: String ): SuccessResponse = maybeResponse( - if (version > 1) { + if (appSettingsService.getApiVersion() > 1) { createSource2(title, url, spout, tags, filter) } else { createSource(title, url, spout, tags, filter) } ) - suspend fun createSource( + private suspend fun createSource( title: String, url: String, spout: String, @@ -184,8 +237,13 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { filter: String ): HttpResponse = client.submitForm( - url = url("/source?username=${appSettingsService.getUserName()}&password=${appSettingsService.getPassword()}"), + url = url("/source"), formParameters = Parameters.build { + // TODO: test this + if (!shouldHavePostLogin()) { + append("username", appSettingsService.getUserName()) + append("password", appSettingsService.getPassword()) + } append("title", title) append("url", url) append("spout", spout) @@ -194,7 +252,7 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { } ) - suspend fun createSource2( + private suspend fun createSource2( title: String, url: String, spout: String, @@ -202,8 +260,12 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { filter: String ): HttpResponse = client.submitForm( - url = url("/source?username=${appSettingsService.getUserName()}&password=${appSettingsService.getPassword()}"), + url = url("/source"), formParameters = Parameters.build { + if (!shouldHavePostLogin()) { + append("username", appSettingsService.getUserName()) + append("password", appSettingsService.getPassword()) + } append("title", title) append("url", url) append("spout", spout) @@ -214,7 +276,9 @@ class SelfossApi(private val appSettingsService: AppSettingsService) { suspend fun deleteSource(id: Int): SuccessResponse = maybeResponse(client.delete(url("/source/$id")) { + if (!shouldHavePostLogin()) { parameter("username", appSettingsService.getUserName()) parameter("password", appSettingsService.getPassword()) - }) + } + }) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/AppSettingsService.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/AppSettingsService.kt index 3c25bbc..0ebce84 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/AppSettingsService.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/AppSettingsService.kt @@ -44,10 +44,6 @@ class AppSettingsService { refreshUserSettings() } - fun logApiCalls(message: String) { - Napier.d(message, tag = "LogApiCalls") - } - fun getApiVersion(): Int { if (_apiVersion == -1) { refreshApiVersion()