mirror of
https://github.com/Suwayomi/Tachidesk.git
synced 2025-12-10 06:42:07 +01:00
Support Token Expiry properly (#878)
* Support token expiry properly * Small fix * Lint * Use newer fixes for expiry * Lint
This commit is contained in:
@@ -67,6 +67,19 @@ class TrackerScoresDataLoader : KotlinDataLoader<Int, List<String>> {
|
||||
}
|
||||
}
|
||||
|
||||
class TrackerTokenExpiredDataLoader : KotlinDataLoader<Int, Boolean> {
|
||||
override val dataLoaderName = "TrackerTokenExpiredDataLoader"
|
||||
|
||||
override fun getDataLoader(): DataLoader<Int, Boolean> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
future {
|
||||
ids.map { id ->
|
||||
TrackerManager.getTracker(id)?.getIfAuthExpired()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TrackRecordsForMangaIdDataLoader : KotlinDataLoader<Int, TrackRecordNodeList> {
|
||||
override val dataLoaderName = "TrackRecordsForMangaIdDataLoader"
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import suwayomi.tachidesk.graphql.dataLoaders.TrackRecordsForTrackerIdDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.TrackerDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.TrackerScoresDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.TrackerStatusesDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.TrackerTokenExpiredDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.UnreadChapterCountForMangaDataLoader
|
||||
|
||||
class TachideskDataLoaderRegistryFactory {
|
||||
@@ -71,6 +72,7 @@ class TachideskDataLoaderRegistryFactory {
|
||||
TrackerDataLoader(),
|
||||
TrackerStatusesDataLoader(),
|
||||
TrackerScoresDataLoader(),
|
||||
TrackerTokenExpiredDataLoader(),
|
||||
TrackRecordsForMangaIdDataLoader(),
|
||||
DisplayScoreForTrackRecordDataLoader(),
|
||||
TrackRecordsForTrackerIdDataLoader(),
|
||||
|
||||
@@ -49,6 +49,10 @@ class TrackerType(
|
||||
fun trackRecords(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<TrackRecordNodeList> {
|
||||
return dataFetchingEnvironment.getValueFromDataLoader<Int, TrackRecordNodeList>("TrackRecordsForTrackerIdDataLoader", id)
|
||||
}
|
||||
|
||||
fun isTokenExpired(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<Boolean> {
|
||||
return dataFetchingEnvironment.getValueFromDataLoader<Int, Boolean>("TrackerTokenExpiredDataLoader", id)
|
||||
}
|
||||
}
|
||||
|
||||
class TrackStatusType(
|
||||
|
||||
@@ -5,6 +5,7 @@ import okhttp3.OkHttpClient
|
||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
|
||||
abstract class Tracker(val id: Int, val name: String) {
|
||||
val trackPreferences = TrackerPreferences
|
||||
@@ -81,6 +82,14 @@ abstract class Tracker(val id: Int, val name: String) {
|
||||
) {
|
||||
trackPreferences.setTrackCredentials(this, username, password)
|
||||
}
|
||||
|
||||
fun getIfAuthExpired(): Boolean {
|
||||
return trackPreferences.trackAuthExpired(this)
|
||||
}
|
||||
|
||||
fun setAuthExpired() {
|
||||
trackPreferences.setTrackTokenExpired(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun String.extractToken(key: String): String? {
|
||||
@@ -93,3 +102,7 @@ fun String.extractToken(key: String): String? {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
class TokenExpired : IOException("Token is expired, re-logging required")
|
||||
|
||||
class TokenRefreshFailed : IOException("Token refresh failed")
|
||||
|
||||
@@ -16,6 +16,12 @@ object TrackerPreferences {
|
||||
|
||||
fun getTrackPassword(sync: Tracker) = preferenceStore.getString(trackPassword(sync.id), "")
|
||||
|
||||
fun trackAuthExpired(tracker: Tracker) =
|
||||
preferenceStore.getBoolean(
|
||||
trackTokenExpired(tracker.id),
|
||||
false,
|
||||
)
|
||||
|
||||
fun setTrackCredentials(
|
||||
sync: Tracker,
|
||||
username: String,
|
||||
@@ -25,6 +31,7 @@ object TrackerPreferences {
|
||||
preferenceStore.edit()
|
||||
.putString(trackUsername(sync.id), username)
|
||||
.putString(trackPassword(sync.id), password)
|
||||
.putBoolean(trackTokenExpired(sync.id), false)
|
||||
.apply()
|
||||
}
|
||||
|
||||
@@ -38,14 +45,22 @@ object TrackerPreferences {
|
||||
if (token == null) {
|
||||
preferenceStore.edit()
|
||||
.remove(trackToken(sync.id))
|
||||
.putBoolean(trackTokenExpired(sync.id), false)
|
||||
.apply()
|
||||
} else {
|
||||
preferenceStore.edit()
|
||||
.putString(trackToken(sync.id), token)
|
||||
.putBoolean(trackTokenExpired(sync.id), false)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun setTrackTokenExpired(sync: Tracker) {
|
||||
preferenceStore.edit()
|
||||
.putBoolean(trackTokenExpired(sync.id), true)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun getScoreType(sync: Tracker) = preferenceStore.getString(scoreType(sync.id), Anilist.POINT_10)
|
||||
|
||||
fun setScoreType(
|
||||
@@ -63,5 +78,7 @@ object TrackerPreferences {
|
||||
|
||||
private fun trackToken(trackerId: Int) = "track_token_$trackerId"
|
||||
|
||||
private fun trackTokenExpired(trackerId: Int) = "track_token_expired_$trackerId"
|
||||
|
||||
private fun scoreType(trackerId: Int) = "score_type_$trackerId"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package suwayomi.tachidesk.manga.impl.track.tracker.anilist
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import suwayomi.tachidesk.manga.impl.track.tracker.TokenExpired
|
||||
import java.io.IOException
|
||||
|
||||
class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor {
|
||||
@@ -17,6 +18,9 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
if (anilist.getIfAuthExpired()) {
|
||||
throw TokenExpired()
|
||||
}
|
||||
val originalRequest = chain.request()
|
||||
|
||||
if (token.isNullOrEmpty()) {
|
||||
@@ -26,9 +30,9 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
|
||||
oauth = anilist.loadOAuth()
|
||||
}
|
||||
// Refresh access token if null or expired.
|
||||
if (oauth!!.isExpired()) {
|
||||
anilist.logout()
|
||||
throw IOException("Token expired")
|
||||
if (oauth?.isExpired() == true) {
|
||||
anilist.setAuthExpired()
|
||||
throw TokenExpired()
|
||||
}
|
||||
|
||||
// Throw on null auth.
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package suwayomi.tachidesk.manga.impl.track.tracker.myanimelist
|
||||
|
||||
import eu.kanade.tachiyomi.AppInfo
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import suwayomi.tachidesk.manga.impl.track.tracker.TokenExpired
|
||||
import suwayomi.tachidesk.manga.impl.track.tracker.TokenRefreshFailed
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
|
||||
@@ -13,51 +16,27 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
|
||||
private var oauth: OAuth? = null
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
if (myanimelist.getIfAuthExpired()) {
|
||||
throw TokenExpired()
|
||||
}
|
||||
val originalRequest = chain.request()
|
||||
|
||||
if (token.isNullOrEmpty()) {
|
||||
throw IOException("Not authenticated with MyAnimeList")
|
||||
}
|
||||
if (oauth == null) {
|
||||
oauth = myanimelist.loadOAuth()
|
||||
}
|
||||
// Refresh access token if expired
|
||||
if (oauth != null && oauth!!.isExpired()) {
|
||||
setAuth(refreshToken(chain))
|
||||
if (oauth?.isExpired() == true) {
|
||||
refreshToken(chain)
|
||||
}
|
||||
|
||||
if (oauth == null) {
|
||||
throw IOException("No authentication token")
|
||||
throw IOException("MAL: User is not authenticated")
|
||||
}
|
||||
|
||||
// Add the authorization header to the original request
|
||||
val authRequest =
|
||||
originalRequest.newBuilder()
|
||||
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||
.header("User-Agent", "Suwayomi v${AppInfo.getVersionName()}")
|
||||
.build()
|
||||
|
||||
val response = chain.proceed(authRequest)
|
||||
val tokenIsExpired =
|
||||
response.headers["www-authenticate"]
|
||||
?.contains("The access token expired") ?: false
|
||||
|
||||
// Retry the request once with a new token in case it was not already refreshed
|
||||
// by the is expired check before.
|
||||
if (response.code == 401 && tokenIsExpired) {
|
||||
response.close()
|
||||
|
||||
val newToken = refreshToken(chain)
|
||||
setAuth(newToken)
|
||||
|
||||
val newRequest =
|
||||
originalRequest.newBuilder()
|
||||
.addHeader("Authorization", "Bearer ${newToken.access_token}")
|
||||
.build()
|
||||
|
||||
return chain.proceed(newRequest)
|
||||
}
|
||||
|
||||
return response
|
||||
return chain.proceed(authRequest)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,23 +49,36 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
|
||||
myanimelist.saveOAuth(oauth)
|
||||
}
|
||||
|
||||
private fun refreshToken(chain: Interceptor.Chain): OAuth {
|
||||
val newOauth =
|
||||
runCatching {
|
||||
val oauthResponse = chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!))
|
||||
private fun refreshToken(chain: Interceptor.Chain): OAuth =
|
||||
synchronized(this) {
|
||||
if (myanimelist.getIfAuthExpired()) throw TokenExpired()
|
||||
oauth?.takeUnless { it.isExpired() }?.let { return@synchronized it }
|
||||
|
||||
if (oauthResponse.isSuccessful) {
|
||||
with(json) { oauthResponse.parseAs<OAuth>() }
|
||||
val response =
|
||||
try {
|
||||
chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!))
|
||||
} catch (_: Throwable) {
|
||||
throw TokenRefreshFailed()
|
||||
}
|
||||
|
||||
if (response.code == 401) {
|
||||
myanimelist.setAuthExpired()
|
||||
throw TokenExpired()
|
||||
}
|
||||
|
||||
return runCatching {
|
||||
if (response.isSuccessful) {
|
||||
with(json) { response.parseAs<OAuth>() }
|
||||
} else {
|
||||
oauthResponse.close()
|
||||
response.close()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (newOauth.getOrNull() == null) {
|
||||
throw IOException("Failed to refresh the access token")
|
||||
.getOrNull()
|
||||
?.also {
|
||||
this.oauth = it
|
||||
myanimelist.saveOAuth(it)
|
||||
}
|
||||
?: throw TokenRefreshFailed()
|
||||
}
|
||||
|
||||
return newOauth.getOrNull()!!
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user